diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel index d7025695e..49f33a8c6 100644 --- a/luci-app-adguardhome/.official_channel +++ b/luci-app-adguardhome/.official_channel @@ -1 +1 @@ -release +both diff --git a/luci-app-appflow/.gitattributes b/luci-app-appflow/.gitattributes new file mode 100644 index 000000000..486c83c76 --- /dev/null +++ b/luci-app-appflow/.gitattributes @@ -0,0 +1,4 @@ +# Everything in this repo targets Linux (OpenWrt). Enforce LF everywhere. +* text=auto eol=lf +*.png binary +*.svg text eol=lf diff --git a/luci-app-appflow/.gitignore b/luci-app-appflow/.gitignore new file mode 100644 index 000000000..cbd8b9831 --- /dev/null +++ b/luci-app-appflow/.gitignore @@ -0,0 +1,7 @@ +*.swp +*.tmp +.DS_Store +node_modules/ + +# Regents review verdicts. Written into the repo being reviewed; never committed. +.regents/ diff --git a/luci-app-appflow/CONTRIBUTING.md b/luci-app-appflow/CONTRIBUTING.md new file mode 100644 index 000000000..81f70cb07 --- /dev/null +++ b/luci-app-appflow/CONTRIBUTING.md @@ -0,0 +1,219 @@ +# Contributing to luci-app-appflow + +Contributions are welcome, including bug reports, and especially the two +hardware tests listed under *Where help is most useful* below. + +## Reporting a problem + +Useful reports include: + +- Device and architecture, OpenWrt release, and `netifyd` version + (`apk info netifyd` or `opkg info netifyd`). +- What the dashboard showed versus what you expected. +- `logread -e appflowd` output covering the period. +- `ubus call appflow status`, which reports the daemon's own counters. The + useful ones are `flows.seen`, `flows.late`, `flows.shed`, `flows.purged`, + `flows.no_total`, `bytes.leaked` and `aggregates.refused`; between them they + distinguish most classes of problem immediately. + **Do not read `flows.dropped`**: it is a last-resort backstop that no input + can move, because the cap-pressure prune always frees space before it is + reached. `flows.shed` is the counter that says the flow table is too small. + +If the complaint is about *where bytes were attributed*, read +[docs/DESIGN.md](docs/DESIGN.md) 2.5 first. Some attribution behaviour is +measured, understood and documented rather than accidental, and knowing which +case you are looking at saves everyone time. + +## Building + +This repository is a single LuCI package, with its `Makefile` at the root and +`icons/` as a second package beside it. That is the same layout upstream +`openwrt/luci` uses under `applications/`, and it drops straight into an +OpenWrt SDK tree's `package/` directory, which the build system scans +recursively. + +It is **not** a feed. A feed is a directory *containing* package directories, +and adding this repo directly with `src-link` produces an empty feed index. +The CI workflow stages the packages into a temporary feed directory for +exactly this reason; see `.github/workflows/build.yml`. + + # from an OpenWrt SDK tree matching your target release + ./scripts/feeds update -a + ./scripts/feeds install -a + git clone https://github.com/VolanticSystems/luci-app-appflow.git \ + package/luci-app-appflow + make package/luci-app-appflow/compile V=s + make package/luci-app-appflow-icons/compile V=s + +CI builds both packages against the OpenWrt SDK on every push and pull +request, so a build break shows up without anyone having to reproduce it +locally. + +## Testing a change on a device + +The daemon is a single ucode script and the views are plain JS, so the +edit/test loop does not require a rebuild: + + # syntax-check the daemon before installing it + ucode -c /usr/sbin/appflowd + + # after replacing a view under /www/luci-static/resources/view/appflow/ + rm -f /tmp/luci-indexcache.* + /etc/init.d/rpcd restart && /etc/init.d/uhttpd restart + +Please confirm a change on real hardware before sending it. This package +talks to a DPI engine whose behaviour is version-specific, and several +plausible-looking assumptions about netifyd have already turned out to be +wrong when measured (documented in DESIGN 2.4 and 2.5). + +**Then run the suites.** The JavaScript one needs only Node and runs anywhere; +the two shell suites need a sandbox router and must be run one at a time. + + node tests/frontend-suite.js # 59 checks, no router needed + sh tests/protocol-suite.sh # 104 checks, sandbox router, no traffic + sh tests/hardware-suite.sh # 15 checks, sandbox router, real traffic + +`protocol-suite.sh` takes a group name: conservation, protocol, bounds, +attribution, hostile, lifecycle, acl, ai, hostmap, drilldown, or all +(the default). + +**`hardware-suite.sh` skips its wire-ratio check when netifyd does not +capture every default-route interface**, and says which one. That check +compares the test client's own veth counter against what appflow counted, +so it silently assumes netifyd watches the path the traffic takes. On a +router with a second uplink or a VPN outside netifyd's capture list the +ratio drops and reads exactly like an attribution defect. It is not one, +and it cost real time on 2026-08-27 before the precondition was made +explicit. + +`frontend-suite.js` loads the REAL `common.js` under Node through +`tests/luci-module.js`, a small `vm.Script` loader that supplies LuCI's +runtime (`_()`, `E()`, `uci`) and nothing else. No logic from the product is +copied into it. Every check in its first three groups is a regression test for +a defect that was real, shipped and fixed: a status pill that read "connected" +from a payload carrying no evidence either way, a colour lookup that a device +named `constructor` could turn into `undefined`, and a normaliser that was not +idempotent. All three were found by people reading the code, because until +2026-08-27 no test could run any of it. + +`protocol-suite.sh` repoints `appflow.socket_path` at a socket it controls and +feeds the real daemon a chosen event stream, so the byte arithmetic is checked +against hand-computed totals rather than a tolerance band, and the error paths +a real agent never produces get exercised. `hardware-suite.sh` drives real +traffic and compares against the client interface's own counter in +`/sys/class/net`, which nothing in this daemon can influence. Neither replaces +the other. + +Both restore what they change on exit, including on interrupt and on a dropped +SSH session, and both take an exclusive lock: they mutate global router state +and running two at once corrupts each other's fixtures. + +**If you add a check, write the sabotage first.** Every check in these files +carries a `SABOTAGE:` comment naming the smallest edit to the *product* that +turns it red, and the comment was written before the assertion. That ordering +is the point: "what does this test check" answers itself in the author's own +words, which is how a test that cannot fail gets written and then read a dozen +times without anyone noticing. If you cannot construct a red state, you have +found something more interesting than a test. + +## Code style + +Match the surrounding code. Specifically: + +- **ucode**, not shell, for daemon logic. Note ucode's integer division and + the requirement to declare functions before use. +- **No build step for the frontend.** The views are hand-written JS using + LuCI's own `form`, `ui`, `rpc`, `poll` and `dom` modules, and the charts are + hand-drawn inline SVG. Please do not introduce a bundler, a framework, or a + charting dependency. +- Keep `appflowd`'s accounting funnelled through the single `account()` choke + point. Byte conservation is a verified property of this package and it is + easy to break by accounting somewhere else. +- SPDX headers on new files, Apache-2.0. + +## Commits + +Prefix the subject with the package name and keep it lowercase after the +colon, matching OpenWrt convention: + + luci-app-appflow: fix device table sort on equal byte counts + +If you would like the change to be portable upstream to `openwrt/luci` later, +add a `Signed-off-by:` line with your real name, which is what upstream's DCO +requires. + +## Translations + +The string catalogue lives in `po/templates/appflow.pot`. It is generated from +the source, not written by hand: every `_('...')` call in +`htdocs/luci-static/resources/view/appflow/*.js`, plus the `title` and +`description` values in `root/usr/share/luci/menu.d/*.json` and +`root/usr/share/rpcd/acl.d/*.json`. That file selection and those keywords are +LuCI's, from `build/i18n-scan.pl` in the `openwrt/luci` tree. + +To add a language, create `po//appflow.po` from the template and +translate the `msgstr` lines. **Nothing in `Makefile` needs to change.** +`luci.mk` discovers languages by globbing `po/*`: + +```make +LUCI_LANGUAGES := $(sort $(filter-out templates,$(notdir $(wildcard ${CURDIR}/po/*)))) +``` + +and generates a `luci-i18n-appflow-` package for each one, so a new +directory is the whole of the work. The one-package-per-language split is +deliberate upstream policy, not an oversight: a router with 8 MB of flash +should not carry forty translations to use one. A combined "all languages" +package [was proposed and +rejected](https://github.com/openwrt/luci/issues/4075), because translations +depend on applications rather than the reverse and no package metadata can +express that. + +Two things worth knowing before you start: + +- **Keep the `%s` and `%d` specifiers, in a working order.** Several strings + interpolate values, and a catalogue that reorders or drops one produces a + broken sentence at best. +- **Category names are not in the template, and that is not a bug.** They + arrive at runtime from netifyd's own `netify-categories.json`, so they exist + in neither the JavaScript nor the daemon as literals and no scanner can find + them. Translating them needs a source-side change first; open an issue rather + than adding orphan `msgid` entries, which the next regeneration would delete. + +If this package is ever accepted into `openwrt/luci`, translations arrive +through [Hosted Weblate](https://hosted.weblate.org/projects/openwrt/) instead +and this section stops being the interesting path. + +## Where help is most useful + +Two open items need hardware the author does not have. Both are documented in +[docs/DESIGN.md](docs/DESIGN.md) 8, with the exact observation that would +settle them: + +1. **NAT dual-capture attribution.** Needs a real client routed *through* the + device so netifyd produces both an internal (`br-lan`) and an external + (`wan`) capture of the same flow. The deciding question is what netifyd + 4.4.7 puts in `local_mac` / `other_mac` when `ip_nat` is true, which + determines whether the current rule double-counts or drops. +2. **Late re-point stranding.** If netifyd first reports a flow unclassified + and identifies it only later, bytes counted before that point stay under + "Unknown". This has not been observed in the wild and its magnitude is + unmeasured. A socket capture correlating per-digest `flow` / + `flow_stats` / detection-update ordering would settle it. + +Reports of either, even without a patch, are genuinely valuable. + +### A note for translators: argument order + +LuCI's `format()` substitutes positionally in order, so a translation that needs +the arguments in a different order than English must say so explicitly. Use +`%1$d`, `%2$s` and so on rather than bare `%d`/`%s`: + + msgid "%d of %d applications" call site passes (shown, total) + wrong "共 %d 个应用中的 %d 个" renders 143 of a total of 7 + right "共 %2$d 个应用中的 %1$d 个" + +**This class of bug passes every check either side can run.** Argument order is +a property of the relationship between the string and its call site, and the +call site is in another file, so a specifier count matches, a markup sweep finds +nothing, and the build is green. Found in the zh_Hans catalogue in PR #1, where +it turned "7 of 143" into "143 of a total of 7". diff --git a/luci-app-appflow/LICENSE b/luci-app-appflow/LICENSE new file mode 100644 index 000000000..d64569567 --- /dev/null +++ b/luci-app-appflow/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/luci-app-appflow/Makefile b/luci-app-appflow/Makefile new file mode 100644 index 000000000..fa8f1d5ad --- /dev/null +++ b/luci-app-appflow/Makefile @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2026 VolanticSystems + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-appflow +PKG_VERSION:=1.1.2 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 +PKG_LICENSE_FILES:=LICENSE +PKG_MAINTAINER:=Bob + +LUCI_TITLE:=Per-application traffic dashboard (DPI via Netify Agent) +LUCI_DESCRIPTION:=Real-time per-application and per-device traffic visibility \ + for OpenWrt, powered by the Netify Agent (netifyd) deep packet inspection \ + engine. A LuCI-native functional equivalent of vendor DPI dashboards. +LUCI_DEPENDS:=+netifyd +luci-base \ + +ucode-mod-socket +ucode-mod-uloop +ucode-mod-ubus +ucode-mod-uci +ucode-mod-fs +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-appflow/README.md b/luci-app-appflow/README.md new file mode 100644 index 000000000..b06e5ff44 --- /dev/null +++ b/luci-app-appflow/README.md @@ -0,0 +1,447 @@ +# luci-app-appflow + +**Real-time per-application traffic visibility for OpenWrt.** + +`appflow` answers the question stock OpenWrt cannot: *what is my network doing +right now, by application?* Netflix vs. YouTube vs. a Windows update, broken +down per device, live in LuCI, built entirely on open components. + +> Status: **working, published as a demonstration project.** Developed and +> tested on real hardware, with byte-attribution behavior measured rather than +> assumed. Read *Known limitations* below before installing: several are +> inherent to the DPI engine, and one affects anyone who restarts the service. +> Not in the OpenWrt feeds yet, so the package below is unsigned. + +## Install + +One file, every device. The package is `noarch` — only JavaScript and ucode +inside — and the same `.apk` is verified installing on both aarch64 and +arm_cortex-a15 hardware. + + wget https://github.com/VolanticSystems/luci-app-appflow/releases/download/v1.1.2/luci-app-appflow-1.1.2-r1.apk + apk add --allow-untrusted ./luci-app-appflow-1.1.2-r1.apk + /etc/init.d/appflowd enable && /etc/init.d/appflowd start + +Needs OpenWrt 25.12 or newer and `netifyd`. `--allow-untrusted` is required +because this is not signed by the OpenWrt build key. Optional icon pack and +Simplified Chinese translation are on the same +[releases page](https://github.com/VolanticSystems/luci-app-appflow/releases). + +Prefer to build it yourself? See *Building from source* below. + +## Screenshots + +Live overview: + +![Overview](docs/overview.png) + +Statistics (past hour): + +![Statistics](docs/statistics.png) + +Filtering. Typing `ai` narrows both lists to AI traffic, and the device panel +switches to what currently-tracked flows account for: + +![Filtering by AI](docs/ai-filter.png) + +## How it works + + netifyd (DPI engine, nDPI-based) + │ JSON flow events (unix socket) + ▼ + appflowd (ucode daemon: aggregates flows → apps × devices) + │ ubus + ▼ + LuCI view (hand-drawn inline SVG charts, live polling) + +There is no database and no polling of the DPI engine: `appflowd` holds a live +aggregate in memory and publishes it over ubus. Design notes, including the +netifyd 4.4.7 event contract as observed on real hardware, are in +[docs/DESIGN.md](docs/DESIGN.md). + +`appflow` itself contacts no network service: it reads a local socket and +publishes over ubus, with no account, key or registration of any kind. + +`netifyd` is a separate package with its own configuration and its own +vendor-supplied signature data, so appflow cannot speak for it. It was measured +rather than assumed. On the development router, after 4 days 23 hours of +uptime, netifyd held 16 sockets: 12 unix and 3 packet capture, and **zero TCP +or UDP sockets**, with no outbound connection at any point. The agent does have +a telemetry sink, and OpenWrt's packaged `/etc/netifyd.conf` ships it turned +off: + + [netifyd] + dump_established_flows = yes + enable_sink = no + +`enable_sink = no` is the line that matters. Check it on your own install +rather than trusting this one: it is netifyd's setting, not appflow's, and +anyone can change it. + +The line above it is quoted only because it is really there. **It does +nothing on 4.4.7**: the string `dump_established_flows` does not appear in the +shipped binary at all, while `enable_sink` does. It is a stale key in the +packaged config, and if you were hoping it means the agent will replay +established flows to a client that connects late, it does not. That is the +cause of the "Unknown" limitation below, not something a setting can turn off. + +## How it compares + +`nlbwmon` is the usual answer to "what is using my bandwidth" on OpenWrt, and +it is the first thing worth checking before installing this. + +| | `nlbwmon` | `appflow` | +|---|---|---| +| Axis | Per host, per protocol and port | Per application, per host | +| How it identifies traffic | Conntrack accounting | DPI, via netifyd | +| Storage | Persistent database, survives reboot | In memory only, live view, keeps nothing | +| Range | Days and months | Live, plus past hour | +| Cost | Under 1 MB VSZ for the daemon | netifyd, around 17 MB VSZ, plus about 2 MB for appflowd | + +They answer different questions, and both were run on the same router at the +same time on identical traffic to check they can be: nlbwmon reported HTTP +9.99 MB and HTTPS 2.09 MB; appflow reported the same traffic as cloudflare, +github and wikipedia. Neither interfered with the other. If you want durable +per-host accounting, nlbwmon does it better and appflow does not attempt it; +if you want to know which *application* is doing it right now, that is the +question this package exists to answer. + +## AI services get their own rows + +netifyd cannot classify AI traffic and will not soon. Its signature set is +dated **August 2023**, carries 199 applications, and contains no AI vendor at +all, so anything from Claude to Midjourney lands in generic HTTP/S. + +The identifying data is already present: netifyd reports the TLS SNI, and +appflow already stores it. So appflow matches that name against a built-in +table of eighty-five AI service domains and gives them their own application +rows and four categories: assistants, media, developer tools and +infrastructure (aggregators, inference APIs, GPU rental, vector stores). + +**This is hostname matching, not DPI, and it is labelled as such.** It asserts +a classification netifyd did not make. netifyd's own answer always wins except +for a handful of services whose parent brand it recognizes and would otherwise +swallow (Gemini under Google, Copilot under GitHub), so the table falls silent +by itself if Netify ever ship AI signatures. + +Matching is anchored on label boundaries, never a substring, so +`anthropic.com.attacker.example` is not labelled Anthropic. It changes an +identity and never a byte: byte conservation is asserted across a batch +containing AI flows. + +Turn it off with `option ai_breakout '0'` in `/etc/config/appflow`. Two things +it cannot do: match a service behind a shared CDN hostname, and survive ECH, +which encrypts the SNI. netifyd has the same ECH exposure. Detail in +[docs/DESIGN.md](docs/DESIGN.md) section 11. + +## Finding things in a busy dashboard + +One streaming session can bury everything else, so both lists filter. + +**Type to narrow.** The box above the application list matches the application +name, its internal key and its category, so `ai` finds every AI service and +`streaming` finds every streaming one. + +**Prefix a word with a minus to exclude it.** `-netflix` shows everything +except Netflix, which is the case a search box alone cannot express: to use it +you would have to already know what you wanted to keep. Terms combine, so +`ai -claude` is AI traffic that is not Claude. + +**Click anything that names something.** A category in the Categories panel, an +application name, or the category under it: each fills the filter box, so the +click is visible, editable and clearable rather than a hidden mode. + +**Click a device to see what it is doing.** The application list then shows only +that device's traffic and a chip appears naming it. The device list has its own +filter box matching name, MAC and IP, with the same minus-to-exclude behavior. + +Both filters compose: pick a device, then type `-netflix`, and you get that +device's traffic minus streaming. + +### What the numbers mean when you drill in + +A device drill-down is computed from the live flow table, not from a stored +per-device-per-application history, so the columns change from **Download and +Upload rates** to **Downloaded and Uploaded bytes** and the caption says +"from flows in progress". Those are the bytes currently-tracked flows account +for: a connection that has already finished is no longer in the table. + +That is a real limitation and it is why the headings change rather than leaving +a byte figure sitting under a column promising a rate. Producing a genuine +per-device-per-application *rate* would need a delta ring for every pair, which +is the unbounded state this design avoids on purpose. + +## Teaching it a service it does not know + +netifyd's signature set is vendor data and it goes stale: the copy shipped with +OpenWrt is dated August 2023. AppFlow recognizes services by the server name +their TLS connections announce, and you can add to that list yourself without +touching any code. + +Add a section to `/etc/config/appflow`: + +``` +config hostmap + option suffix 'torproject.org' + option name 'Tor' + option category 'Privacy' +``` + +then `/etc/init.d/appflowd restart`. Traffic to `www.torproject.org` and +`check.torproject.org` now appears as **Tor** under **Privacy**. + +| option | meaning | +|---|---| +| `suffix` | matched against the last two, three or four labels of the server name, anchored on a label boundary. Must contain a dot. | +| `name` | what appears in the application list. | +| `category` | what appears in the category breakdown. Free text; reuse an existing one to group with it, or invent your own. | +| `strong` | optional, default `0`. `0` fills a gap only, leaving netifyd's own identification alone where it has one. `1` overrides netifyd too, for when it recognizes a parent brand and swallows the service inside it. | + +**The same section overrides a built-in entry**, so this is also how you correct +one that is wrong for your network: + +``` +config hostmap + option suffix 'anthropic.com' + option name 'Work AI' + option category 'Business' +``` + +Matching is anchored on label boundaries, so `torproject.org` matches +`check.torproject.org` and **not** `torproject.org.example.com`, which anyone +can register. A malformed entry is skipped and named in the log rather than +silently ignored, because a typo otherwise looks exactly like the feature not +working: + +``` +logread -e appflowd | grep hostmap +``` + +This is name recognition, not deep packet inspection, and it inherits the same +limits: a service behind a shared CDN hostname is invisible, and ECH would end +it. Thirteen checks in `tests/protocol-suite.sh hostmap` cover it. + +## Known limitations + +Read these before installing. They are measured and documented rather than +left to be discovered. Details and supporting measurements are in +[docs/DESIGN.md](docs/DESIGN.md) sections 2.5 and 8. + +- **All statistics live in memory and do not survive a restart.** There is no + database, by design. Restarting `appflowd`, restarting netifyd, or rebooting + starts the past-hour view from empty. If you need history across restarts, + this package does not currently provide it. +- **Flows in progress across a restart are attributed to "Unknown".** netifyd + 4.4.7 sends counter updates for a flow that was already established when + `appflowd` connected, but never the event carrying its identity, so those + bytes accumulate under "Unknown" until the flow ends. Measured: restarting + the daemon mid-transfer moved 10.7 MB of a correctly classified HTTP flow. + It is bounded to the window after a restart, resolves as connections cycle, + and loses no byte totals. A future version could mitigate the + `appflowd`-restart case by checkpointing its own identity map; the case where + *netifyd* restarts cannot be fixed without support from netifyd. + + Accuracy across a restart was measured in 1.0.2 against the client's own + interface counter: 0.89 and 1.01 of wire truth on two runs, against 0.70 and + 0.79 before that release, with a no-restart control at 1.01. If you want an + exact figure, take it after the restart window rather than across it. +- **Accurate accounting for clients behind NAT needs conntrack.** netifyd + reports the WAN-side capture of a NAT'd flow using the router's own address, + which is indistinguishable from traffic the router originated. appflow + resolves it by looking the flow up in `/proc/net/nf_conntrack`, which is + present on any normal OpenWrt firewall install. Measured against interface + counters: client traffic 99.8% and router traffic 97.9% of actual, each + counted once. **If conntrack cannot be read**, appflow keeps client + accounting correct and stops attributing the router's own traffic, rather + than double-counting; it logs one warning saying so. Check + `ubus call appflow status` under `conntrack` to see which mode you are in. +- **Late re-classification may leave early bytes under "Unknown".** If netifyd + identifies a flow only after several packets, bytes counted before that point + are not retroactively moved, because the accounting path is deliberately + append-only. Not observed in practice and currently unquantified. +- **Flow offloading is untested.** appflow has not been run on a router with + software or hardware flow offloading enabled. netifyd captures packets on the + interface rather than through netfilter, so the two paths are not obviously + in conflict, but that is reasoning and not a measurement, and no measurement + has been made. Reports from anyone running offloading are welcome. +- **Detection quality is netifyd's, not ours.** Which applications are + recognized, and how accurately, is a property of the DPI engine and its + signature data. appflow reports what netifyd detects. +- **This inspects traffic.** Consider whether deep packet inspection is + appropriate on your network, and for the people using it, before installing. + +On accounting accuracy: measured against client-side ground truth, appflow +accounted for **at least** every byte transferred and never lost any (25 flows +of 2 MB gave 50,000,000 bytes transferred against 51,127,054 accounted; 150 +flows of 1 KB were all accounted). The roughly 2% excess is consistent with +protocol overhead being counted, but it has not been reconciled packet by +packet, so treat these totals as a close upper bound rather than an exact +byte count. + +**If something looks wrong with the numbers**, run `ubus call appflow status`. +Three fields there answer most questions: + +| field | what a non-zero value means | +|---|---| +| `bytes.leaked` | **Should read 0.** Traffic was reported by the agent and never reached an aggregate. If this moves, please tell me. | +| `aggregates.refused` | A per-class table filled with live entries, so new applications or devices stopped being tracked. The dashboard is incomplete. Nothing is being mis-counted; the totals stay correct. | +| `flows.shed` | The flow table hit `flow_max` and the least recently active flows were evicted to make room. Raise `flow_max` if this climbs steadily. | + +`flows.shed` is counted separately from `flows.pruned`, which is ordinary +housekeeping of idle flows and is expected to be large. Reading them as one +number hides cap pressure underneath normal behavior. + +## Security and privacy + +Traffic monitoring is a privacy question by definition, so here is the whole +surface. Everything below is checkable in `root/usr/sbin/appflowd` and +`root/usr/share/rpcd/acl.d/luci-app-appflow.json`. + +**It runs as root, because it has to.** The daemon reads netifyd's agent socket +and `/proc/net/nf_conntrack`, neither of which is readable otherwise. There is +no privilege drop. If that is not acceptable on your box, do not install it. + +**It reads four files and writes none.** `/etc/netify.d/netify-categories.json`, +`/tmp/dhcp.leases`, `/var/run/netifyd/status.json` and `/proc/net/nf_conntrack`, +all read-only, plus its own UCI config at start-up. It contains no `writefile`, +no shell-out and no `popen`. It opens no listening socket: it connects out to +netifyd's Unix socket and publishes a ubus object, which is the local bus and +not the network. + +**Nothing is stored.** There is no database, no log of who visited what, no +spool file. Every counter lives in memory and dies with the process, so +`/etc/init.d/appflowd restart` is a complete erase. That is also the honest +limitation: there is no history, and restarting the router loses the day. + +**The ubus surface is six read methods and one write, and the write is +`reset`.** The ACL is deliberately narrower than the daemon: + +| method | granted to the web UI | what it returns | +|---|---|---| +| `summary`, `stats`, `status` | yes | totals, time series, health | +| `devices`, `apps` | yes | per-device and per-application aggregates | +| `app_detail` | yes | one application: its devices, and the hostnames it contacted, each with byte totals | +| `flows` | **no** | the live per-connection table, including remote addresses and ports | +| `reset` | yes (write) | clears the counters | + +**`flows` is ungranted on purpose and the reason is written into the ACL file +itself.** That is the difference between a dashboard and a surveillance log. A +LuCI session can see that a device used a streaming service and that the +service contacted a set of hostnames; it cannot pull the connection table and +join a specific device to a specific remote endpoint, because `app_detail` +returns devices and hostnames as two separate lists for one application rather +than as a cross product. + +**What the dashboard does reveal**, so nobody is surprised: DHCP hostnames, MAC +and IP addresses of LAN devices, which applications each device used, and the +TLS SNI hostnames each application contacted. Anyone who can log into LuCI can +see all of it. On a family router that is a real consideration and it is your +call, not the package's. + +**Bounded tables that refuse rather than lie.** Aggregate tables are capped. At +the cap the daemon evicts only entries with no live flow, and when every slot is +busy it refuses to grow rather than dropping data that is still arriving, so the +totals stay correct while the breakdown goes incomplete. That refusal used to be +invisible; it is now counted and reported as `aggregates.refused`, because a +table that has quietly stopped accepting new devices looks identical to a quiet +network. + +**Reporting something.** If you find a problem, open an issue on the repository, +or send it privately if you would rather not post it. Either is fine, and either +gets an answer. + +## Tests + +Three suites, 179 checks, no failures. Two need a sandbox router; one needs +nothing but Node and runs on every push. + +| suite | checks | what it does | +|---|---|---| +| `tests/frontend-suite.js` | 59 | loads the real view code under Node. Mostly regression tests for defects that shipped. | +| `tests/protocol-suite.sh` | 104 | replaces the agent with a socket the test controls, so byte arithmetic is checked against hand-computed totals rather than a tolerance band, and the error paths a real agent never produces get exercised. | +| `tests/hardware-suite.sh` | 16 | drives real traffic and compares against the client interface's own counter in `/sys/class/net`, which nothing in this daemon can influence. | + +Two of the hardware checks SKIP, loudly and counted, when the router cannot +support them: when netifyd is not capturing every default-route interface a +wire-ratio comparison is meaningless rather than merely weak, and the +purge-rate check needs more flows than a quiet bench produces. A skipped +check is reported as skipped, never as a pass. + +Every check names, in a comment written before the assertion, the smallest +edit to the *product* that turns it red, and those sabotages are actually +run. That is what caught three checks in the 2026-08-27 round that could +not fail at all, two of them in the group written to prove the AI matcher +refuses lookalike domains. + + +Every check carries a comment naming the edit to the *product* that turns it +red, written before the assertion. Details in +[CONTRIBUTING.md](CONTRIBUTING.md). + +## Requirements + +- OpenWrt with `netifyd` available (developed and tested against **OpenWrt + 25.12.5** with **netifyd 4.4.7**). +- Enough headroom to run DPI. Measured on the test device under light load: + **netifyd** around 17 MB VSZ, **appflowd** around 2 MB VSZ, negligible CPU + for both. netifyd is the expensive component; appflow adds little on top. + +Tested on a Linksys EA8500 (ipq806x). It is architecture-independent +(`LUCI_PKGARCH:=all`), and CI builds it for `arm_cortex-a15_neon-vfpv4` and +`aarch64_cortex-a53`. + +## Building from source + +Prefer to compile it yourself, or running an OpenWrt release older than +25.12 where the `.apk` above will not install? Build it with the OpenWrt +SDK for your release, then install the resulting package. + +Build: + + # from an OpenWrt SDK tree matching your device's release + ./scripts/feeds update -a + ./scripts/feeds install -a + git clone https://github.com/VolanticSystems/luci-app-appflow.git \ + package/luci-app-appflow + make package/luci-app-appflow/compile V=s + +The optional icon pack is a separate package in the same tree, so build it +separately if you want it: + + make package/luci-app-appflow-icons/compile V=s + +Install on the device (copy the built package over first): + + apk add --allow-untrusted /tmp/luci-app-appflow-[0-9]*.apk + +Note the `[0-9]` in that glob: a plain `luci-app-appflow-*.apk` would also +match `luci-app-appflow-icons-*.apk` and pull in the optional icon pack. + +`--allow-untrusted` is required because a locally built package is not signed +by an OpenWrt repository key. On releases still using opkg, use +`opkg install ./luci-app-appflow_*.ipk` instead. + +### Optional icon pack + +`luci-app-appflow-icons` adds brand icons for the applications netifyd can +detect. Without it the dashboard renders letter-tile avatars, which is a +supported configuration rather than a degraded one. It is packaged separately +so the base package stays small, and so it can be removed on its own if the +third-party brand marks it ships ever need to be. Build it as shown above, +then: + + apk add --allow-untrusted /tmp/luci-app-appflow-icons-[0-9]*.apk + +## A sibling package + +[`luci-app-wansentry`](https://github.com/VolanticSystems/luci-app-wansentry) +generates a two-uplink mwan3 failover configuration from one page, for people +who want a second WAN to take over automatically without assembling six mwan3 +sections by hand. Same author, same OpenWrt release, and it solves a problem +next to this one rather than the same one: appflow tells you what your traffic +is, wansentry keeps it moving when a line drops. Neither depends on the other. + +## License + +Apache-2.0, see [LICENSE](LICENSE). The icon pack is a separate package with +its own licensing; see `icons/licenses/` in that package. diff --git a/luci-app-appflow/docs/DESIGN.md b/luci-app-appflow/docs/DESIGN.md new file mode 100644 index 000000000..989d9d074 --- /dev/null +++ b/luci-app-appflow/docs/DESIGN.md @@ -0,0 +1,984 @@ +# appflow: Design + +Status: v1 spec LOCKED (2026-08-21). Remaining open items tracked in §8. + +## 1. Goal + +A LuCI-native, fully open per-application traffic dashboard for OpenWrt, built +on the Netify Agent. Stock OpenWrt can tell you how much traffic crossed an +interface; it cannot tell you which application produced it. Several vendor +firmwares ship a DPI traffic-statistics screen that can, and this is an open +equivalent for stock OpenWrt. + +Primary scope (v1): **live view**, per-application and per-device rates and +session totals, category grouping, live charts. +Explicit non-goal for v1, but designed-for: **historical accounting** (see §7). + +## 2. Ground truth (verified on hardware, 2026-08-21) + +Test platform: Linksys EA8500, OpenWrt 25.12.5, netifyd 4.4.7 (nDPI 5.0-based), +arch `arm_cortex-a15_neon-vfpv4`. All facts below observed live, not assumed. + +### 2.1 Data source: netifyd JSON export socket + +- netifyd **listens** on `unix:/var/run/netifyd/netifyd.sock` + (`[socket] listen_path[0]`, enabled by default in the OpenWrt package). +- ucode connects with plain `socket.connect(path)`, verified. +- **Wire framing**: newline-delimited JSON where each payload line is preceded + by a header line `{"length": N}`. Parser rule: parse each line; a lone + `length` key = frame header (skip); anything else dispatches on `type`. +- Event types observed: `agent_hello`, `definitions`, `agent_status` (15 s + cadence), `flow`, `flow_stats`. +- `/var/run/netifyd/status.json` contains **agent status only** (plus a useful + MAC→IP `devices` map), no flow data. The socket is the only flow source. + +### 2.2 Event schemas (as observed) + +`flow` (fires on detection; `detection_updated` re-fires on refinement): + +```json +{ "type": "flow", "interface": "wan", "internal": false, + "flow": { + "digest": "9c2d…", // unique flow key + "detected_application": 10465, + "detected_application_name": "netify.openwrt", + "detected_protocol": 5, + "detected_protocol_name": "DNS", + "category": { "application": 31, "protocol": 14, "domain": 0 }, + "host_server_name": "openwrt.org", // SNI/hostname when known + "local_ip": "…", "local_mac": "…", "local_port": 53, + "other_ip": "…", "other_mac": "…", "other_port": 57052, + "other_type": "local" | "remote", + "local_origin": bool, "ip_nat": bool, "ip_version": 4, "ip_protocol": 17, + "first_seen_at": 1787325075857, "last_seen_at": …, // ms epoch + "risks": { "ndpi_risk_score": 10, "risks": [46] } // bonus signal + } } +``` + +`flow_stats` (periodic per-flow counter updates): + +```json +{ "type": "flow_stats", "interface": "wan", "internal": false, + "flow": { "digest": "…", + "local_bytes": 0, "other_bytes": 240, "total_bytes": 480, + "local_packets": 0, "other_packets": 2, "total_packets": 4, + "last_seen_at": 1787325070685 } } +``` + +**Correction (implementation ground truth, 2026-08-21):** `local_bytes` / +`other_bytes` are **per-update deltas** during a flow's life, but on the +final `flow_purge` event for that flow they are **always zero** (confirmed +on 10+ live purge events), while `total_bytes` is the flow's final +cumulative value. Since the purge event cannot supply a final directional +split, appflowd recovers it by distributing the `total_bytes` delta since the +last known total across up/down using the flow's established up/down ratio +(accumulated from that flow's prior `flow_stats` deltas), rather than +accounting `local_bytes`/`other_bytes` from the purge event itself. + +### 2.3 Naming and taxonomy + +- `definitions` event carries id→tag maps: 416 applications, 217 protocols. + Applications and protocols are SEPARATE id spaces that can collide numerically + (id 206 is both `WireGuard` the protocol and `netify.cloudflare` the app), and + legacy netify apps exist below 10000 (netflix=133), the `netify.` tag prefix, + not the id range, is the discriminator. Details: docs/netifyd-4.4.7-interface.md. +- Category **names** live in `/etc/netify.d/netify-categories.json`: + `application_tag_index` (32 tags: `streaming-media`, `social-media`, `games`, + `voip`, `cdn`, `advertiser`, `os-software-updates`, `malware`, …) and + `protocol_tag_index` (18 tags), plus `application_index` / + `protocol_index` mapping category-id → member ids. +- Display-name policy: application name if detected (strip `netify.` prefix, + title-case), else protocol name, else `Unknown`; hostname (`host_server_name`) + shown as secondary detail. +- **Licensing**: the signature DB (`netify-apps.conf`) and generated category + file are eGloo-proprietary data shipped by the `netifyd` package. appflow + reads them at runtime and **vendors none of this data** in the repo. + +### 2.4 Additional confirmed facts (research pass, 2026-08-21) + +- `flow_purge` events carry `reason: closed|expired|terminated` and the + flow's final cumulative `total_bytes`/`total_packets`, but + `local_bytes`/`other_bytes` are always zero on purge (confirmed live, see + §2.2); the final up/down split must be recovered from the accumulated + ratio, not read off the purge event, before flow removal. +- **Corrected:** `dump_established_flows` does NOT exist in netifyd 4.4.7 (string absent from the binary; a research citation of v5-era docs was wrong). On (re)connect a + client starts blind. The daemon + tolerates `flow_stats` for unknown digests (stub "Unknown" entry). **Corrected + 2026-08-22:** that stub is backfilled only when a `flow` event actually follows, + which happens for a genuine stats-before-flow race but NOT for a flow that was + already established at connect time, netifyd never emits a `flow` event for + those, so they stay Unknown for their remaining lifetime. Measured in §2.5. +- App detections are `netify.` strings; protocol names are plain. +- OpenWrt ships netifyd **4.4.7** while upstream is v5.x with a different + plugin architecture, all behavior here is pinned to 4.4.7-as-packaged. +- netifyd bundles its own nDPI statically: `libndpi` is NOT a dependency. + +### 2.5 Byte-attribution behavior (measured 2026-08-22) + +Three questions about where bytes land were settled on the bench (EA8500, +netifyd 4.4.7). **Method matters here:** all earlier attempts were confounded by +upstream hosts throttling the test router's WAN IP under repeated fetches, and a +refused connection carries no SNI, so netifyd cannot classify it and it lands as +Unknown for reasons unrelated to this daemon. The measurements below therefore +use **LAN-local traffic only** (a file served from the router over `br-lan` via +`socat`, pulled by a LAN client with `curl --limit-rate`), so no upstream can +participate and throttling is impossible by construction. + +- **Byte conservation through the Unknown path: HOLDS.** 25 short flows of 2 MB + = 50,000,000 bytes of client-side ground truth against 51,127,054 bytes + accounted (the excess is TCP/HTTP overhead, both directions). Repeated with + 150 *tiny* 1 KB flows (the case most likely to complete inside one + `update_interval`, and so most likely to be purged before any `flow_stats`): + all 150 accounted, `no_total` = 0, nothing lost. A plausible reading of the + `fr.total > 0` gate in `flow_update()` predicts silent loss for such flows; + that prediction is **refuted** on this hardware, because netifyd supplies + accountable byte counts for sub-interval flows. + + **CORRECTED 2026-08-25. The two paragraphs below understated this, and the + review panel was right.** Once `purge_only_rescued` was instrumented and left + running against ordinary traffic, it reported **13 purge-only flows out of + 1,341 seen, 0.97%**. Flows purged without ever receiving a `flow_stats` DO + occur on netifyd 4.4.7. Before the defensive path was added, every one of them + lost its entire byte count silently, which is exactly what three carriers + predicted and what this section briefly called refuted. + + The refutation was under-powered, not wrong-headed: a 38-flow capture at a + 0.97% rate has an expected count of 0.4, so observing none said almost + nothing. Reading "0 of 38" as "netifyd never does this" was the error, and the + lesson is that a negative result needs its sample size checked against the + rate it claims to exclude. The event-level evidence below is still accurate + about what it saw; it simply did not see enough. + + **Re-tested at the EVENT level 2026-08-24, after an adversarial review panel + independently reached the same prediction three times.** The 2026-08-22 result + above is outcome-based: it shows the bytes landed, not why. Tapping netifyd's + export socket directly for 170 s (`flow`/`flow_stats`/`flow_purge` per digest, + UDP DNS and short TCP both driven deliberately), **64 of 64 flows whose full + lifecycle was observed carried a `flow_stats` before their `flow_purge`.** + Zero went `flow` -> `flow_purge` directly. The premise the prediction rests on + does not occur on netifyd 4.4.7. + + Two flows in that capture did show a purge with no preceding stats, and both + were flows that predated the tap: their birth was simply not observed. That is + §2.4 connect-blindness, not this. Mistaking one for the other is easy and is + why the analysis is stated at the level of *fully observed* lifecycles. + + The gate is defended: `flow_update()` takes a `final` flag from `ev_purge()` + and attributes `total_bytes` for a purge-only flow, counted in + `stats.purge_only` and reported as `status.bytes.purge_only_rescued`. That + counter is **not** expected to stay 0 -- it runs at about 1% of flows -- and + it is the reason those bytes are now counted at all. Watch its rate rather + than its presence: a sharp rise means netifyd's event contract has shifted, + and a zero across a busy run means the rescue has regressed. +- **Connect/reconnect blindness: REAL, bounded, not fixable here.** A flow that + is already established when the daemon connects gets `flow_stats` but never a + `flow` event (§2.4), so its identity never arrives and its ongoing bytes + accumulate under Unknown until it ends. Forced deterministically by restarting + the daemon mid-transfer: 10.7 MB of a correctly-classified HTTP flow moved to + Unknown while the app's own total stayed flat, and a fresh socket client + observed 5 `flow_stats` and 0 `flow` events for that flow. Confined to the + window after a daemon restart or a netifyd restart, self-heals as flows cycle, + and the bytes are parked rather than lost. No fix exists on our side without + an established-flow dump from netifyd; see §8. +- **Late re-point stranding: OPEN, see §8.** Distinct from the above and not yet + observed in the wild. + +Counters relevant to all three (`status.flows`) behaved consistently: `dropped` +stayed 0 and `late` stayed flat under sustained load, so the daemon is not +losing events under load; the Unknown mass seen during earlier testing was the +upstream confound plus the restart blindness above, not an event-handling defect. + +## 3. Architecture + +``` +netifyd (DPI, procd service; nDPI engine) + │ unix stream, length-framed JSON events + ▼ +appflowd — ucode daemon (procd service) + • connects to netifyd.sock, reconnects with backoff + • parses frames, maintains flow table keyed by digest + • aggregates: totals + windowed rates per (application × device), + per category, per device; prunes idle flows + • serves snapshots over ubus ←— single writer, many readers + ▼ +rpcd ACL (usr/share/rpcd/acl.d) gates LuCI session access + ▼ +LuCI JS view (htdocs/…/view/appflow/*.js) + • L.poll every 3–5 s → ubus call → render + • hand-drawn inline SVG charts (theme-aware via CSS custom properties); + standard LuCI tables/widgets +``` + +Two implementation options for the ubus surface: + +- **A (preferred): appflowd registers a ubus object directly** via + `ucode-mod-ubus` (`conn.publish()`); rpcd ACL still governs LuCI access. +- **B (fallback): snapshot file**, appflowd atomically writes + `/var/run/appflow/state.json`; a stateless rpcd-ucode plugin serves reads. + +**DECIDED: Option A shipped**, `publish()` proven reliable on 25.12 (typed args, +status returns, int64 all verified on-device). Option B text retained above only as design rationale. + +### 3.1 Why a persistent daemon (not an on-demand reader) + +The socket is a **stream**: flows announce once, then only counter deltas +follow. Any on-demand reader would miss all context before it connected. +State must live in a long-running consumer; ucode + uloop keeps that consumer +tiny (no C, no external deps beyond packaged ucode modules). + +### 3.2 Flow attribution and the dual-capture problem + +netifyd watches `br-lan` (internal) and `wan` (external) simultaneously, so a +NAT'd client flow can be captured **twice** (pre- and post-NAT, different +digests, one per interface). The device a flow belongs to and whether it is a +duplicate must be decided without double-counting. + +**The shipping rule (implemented in `flow_identify`).** An earlier design paired +the two captures on the remote endpoint and keyed the dedup on `ip_nat`; it was +replaced before v1 because that pairing was fragile (see §10). The rule that +ships is deterministic and needs no cross-flow pairing state: + +- **Device side.** On an `internal` capture the device is the non-router + endpoint; on an `external` capture the device is the router-owned endpoint + (upstream infrastructure is the peer). `is_router()` decides which side is the + router, from the interface MAC/IP maps learned from `agent_status`. +- **Multicast/broadcast** endpoints collapse to a single `multicast` + pseudo-device (via netifyd's `other_type` when present, else the group-bit MAC + test), so they cannot mint device aggregates. +- **Single-count (shadow) rule.** An `internal` capture is always counted (it is + the device's own copy). An `external` capture is counted only when the device + is the router itself; an external capture of a *client* flow is a NAT twin and + is **shadowed** (tracked, never counted). This is gated on `saw_internal`, a + process-wide latch meaning "internal capture is active at all": on an + external-only netifyd (`-E` with no `-I`) nothing is shadowed, so every flow is + counted rather than dropped to zero. + +**What this rule is and is not.** It is a *mode* test ("is internal capture +running"), not a *per-flow twin* test ("does this specific flow have an internal +twin"). The two differ only for flows that appear on the WAN side alone, and +`fr.nat` (`ip_nat`) is retained as a diagnostic field in the `flows` method but +drives no decision. + +**FIXED 2026-08-24 by consulting conntrack. The history below is kept because +the mistakes are the instructive part.** + +netifyd cannot answer this question, so appflowd now asks the kernel. For each +external capture whose device resolves to the router, `ct_nat_twin()` looks the +flow up in `/proc/net/nf_conntrack` on its reply tuple and compares the ORIGINAL +source against the reply destination: they differ for a masqueraded client and +match for router-originated traffic. Snapshots are reused for `CT_TTL_MS`, and a +lookup miss forces one refresh (floored at `CT_FORCE_MIN_MS`) because a flow is +normally younger than the current snapshot. If conntrack cannot be read at all +the code shadows on the safe default and says so once in the log, which keeps +client accounting correct at the cost of not attributing the router's own +traffic. + +Measured after the fix, against wire ground truth from interface counters: + +| | wire truth | attributed | | +|---|---|---|---| +| NAT-ed client | 21,323,454 | 21,287,298 | 99.8% | +| router's own | 6,290,088 | 6,157,849 | 97.9% | + +with `conntrack: available true, twins 20, router_local 2, unresolved 2`. Both +are counted once. The fallback path was tested by pointing `CONNTRACK_FILE` at a +non-existent path: totals stayed at 97% rather than doubling, `unresolved` rose +to 16, and the warning fired. + +**A defect found in the first version of this very fix**, worth recording because +it would have been invisible: caching the snapshot for 2 s meant new flows, +which are exactly the ones needing classification, usually missed it. It looked +like it worked (totals were right, because everything got shadowed) while +silently degrading to the no-conntrack behavior with conntrack fully available. +`unresolved: 20` against `router_local: 0` was the tell. + +--- + +**The original defect, for the record. This used to double-count.** An earlier revision of +this section, committed the same day, claimed the opposite and said the twin +was correctly shadowed. That claim rested on a single measurement and was +wrong; the correction and how it happened are recorded at the end of this +section, because the mistake is more instructive than the result. + +The deciding fact was what netifyd 4.4.7 puts in `local_mac`/`other_mac` when +`ip_nat` is true. Measured: **it carries the router's own WAN identity, not the +client's.** A NAT'd flow's external +capture reports + +```json +{ "ip_nat": true, "local_ip": "192.168.72.10", + "local_mac": "c0:56:27:4e:3e:92", "local_origin": true, + "other_ip": "172.66.0.218" } +``` + +where `192.168.72.10` / `c0:56:27:4e:3e:92` are this router's WAN address and +MAC, while the internal capture of the same flow carries the real client +(`192.168.1.50`). + +That makes `dev_is_router` **true** on the twin, and the rule reads + +``` +want_shadow = (saw_internal && !fr.internal && !dev_is_router && !group) +``` + +so `!dev_is_router` is false and the twin is **NOT shadowed**. It is counted a +second time, under the `router` pseudo-device, on top of the internal capture +already counted against the real client. The global total roughly doubles for +NAT'd client traffic. + +Measured with a genuine NAT'd client: a veth pair into a network namespace +attached to `br-lan`, addressed `192.168.1.50/24` with a default route through +the router, so its traffic is really forwarded and really NATed. Ground truth +is the veth interface byte counter, which is **wire** bytes, the same quantity +appflow counts. + +Per-device attribution of the client itself is essentially exact. 25 flows of +2 MB: + +| | bytes | +|---|---| +| wire ground truth to/from the client | 53,145,843 | +| appflow, client device | 53,145,347 | + +496 bytes of difference in 53 MB. The client's own row is trustworthy. + +**The global total is not.** The same run credited the `router` pseudo-device a +further 51,785,387 bytes, the external twin of the very same traffic. Three +clean single-flow trials, each restarting `appflowd` first and letting the flow +purge: + +| run | wire truth | appflow grand total | ratio | +|---|---|---|---| +| 1 | 10,479,105 | 17,089,074 | 163% | +| 2 | 10,478,155 | 20,639,013 | 196% | +| 3 | 10,478,879 | 20,877,799 | 199% | + +> **The rest of this subsection is HISTORICAL and describes the state before +> the fix above. It is kept because the reasoning is what led to the fix, but +> nothing below is an open problem.** A review panel reading this document cold +> reported it as a live double-count, having entered mid-section, so if you are +> skimming, stop here and go to §3.3. + +**Fix direction, at the time this was written, since implemented.** The `!dev_is_router` term exists for a +real reason: traffic the router genuinely originates belongs under `router` and +must not be shadowed. So it cannot simply be dropped. What is needed is a way +to tell a router-originated flow from a NAT'd client's twin, both of which +present the router's own MAC on the external capture. `ip_nat` looks like the +obvious discriminator and `fr.nat` already carries it, but it is **not +reliable**: a capture taken while generating both kinds of traffic reported +`ip_nat: false` for every flow including the NAT'd client's, even though +`ip_nat: true` was definitely observed on a router-WAN flow earlier the same +day. Any fix must first establish when netifyd actually sets that field. + +**How the wrong conclusion got committed, because this is the useful part.** +The first measurement was a single 20 MB flow that reported the client at +20,950,037 bytes and `router` at 452. That looked decisive and was written up +as resolved. It was not reproducible: repeating the identical single-flow test +gave `router` 20,572,398 and a grand total of 167%. One measurement, no +repetition, and a result that happened to match the hoped-for branch. The same +failure this project has caught in reviewers repeatedly, committed by the +author, on the day it was caught elsewhere. + +**Methodology warning for anyone repeating this.** An earlier run of the same +test reported 69% and then a spread of 46-101%, which would have been a serious +false finding. Both were the measurement, not the daemon: the first sampled +before the flow had purged and immediately after an `appflowd` restart (so it +also measured connect-blindness, §2.5), and the spread came from fragile shell +parsing picking the wrong record out of the JSON. Let `appflowd` settle before +the flow starts, let the flow purge before sampling, and read the numbers +directly rather than through `grep`. + +**Related sub-case, now mitigated.** A pre-existing external flow captured only +as a stub (`flow_stats` before any `flow` event) is identified once, before +`saw_internal` latches, and would otherwise stay counted alongside its internal +twin for its whole life, inflating the central total. `reshadow()` re-applies +the shadow rule on later stats updates, closing that window to at most one stats +interval. The residual sign question for an unidentifiable stub (a genuine NAT +twin vs. pre-existing router-origin traffic, both landing in the "unknown" +bucket) folds into the same bench test above. + +### 3.3 Memory bounds (runs on 128 MB devices) + +Flow table capped (default 4096 flows, uci-tunable); LRU-pruned on purge +events, idle timeout, and cap pressure. No unbounded growth by construction, +but the real worst case for the aggregate tables is not small: each of +`AGG_MAX` (384) aggregates per class can carry up to `APP_DEV_MAX` (64) +per-device ring objects, a ceiling of 384 x 64 = 24,576 per-(app x device) +ring objects, plus up to `APP_HOST_MAX` (16) host ring entries per aggregate, +384 x 16 = 6,144 more. Observed RSS was approximately 2 MB at a light load of +33 apps and 11 devices (§8); the structural ceiling above is one to two +orders of magnitude higher than that observed point, not a number the box is +expected to reach under normal use, but worth stating plainly rather than +waved off as "naturally small". + +**Every bound must be VISIBLE when it binds, and two of the three were not.** +A table that silently stops accepting data reports the same status as a table +with nothing to accept, which is the worst reading an operator can be given: +the dashboard looks healthy at exactly the moment it has become incomplete. + +| bound | counter | added | +|---|---|---| +| aggregate table full of live entries | `aggregates.refused` | 2026-08-25 | +| flow table at `flow_max` | `flows.shed` | 2026-08-26 | +| unknown event-type map | (bounded at 16, not counted) | | + +`flows.shed` counts evictions caused by **cap pressure**, separately from +`flows.pruned`, which is ordinary idle housekeeping and is expected to be +large. They shared one counter until 2026-08-26, so cap pressure was invisible +underneath normal behavior. + +**`flows.dropped` is a genuine last-resort backstop and reads 0 in practice.** +`flow_new()` increments it only if the table is still full *after* +`prune(now, true)`, and that prune unconditionally sheds `int(flow_max/10) + 1` +entries, so it always frees space and the fall-through does not occur. +Measured by `tests/protocol-suite.sh`: 200 distinct digests against +`flow_max=64` gave `tracked=60 pruned=143 shed=143 dropped=0`. It is kept +because the guard is correct and cheap; it is not the number to watch. + +The conventions this screen follows, which are the ones a DPI traffic screen +conventionally uses and which users of such screens already expect: a +**range-based** view (past hour, with day and week as later phases), a **15 s** +poll, table columns of Application / Download / Upload / Total sorted by total +descending, searchable, with a synthetic "All traffic" first row, a Top-10 bar +chart, and a per-app detail drawer carrying a time series plus per-client rows +(name, MAC, last seen, share %). + +Classification here is **entirely local**: netifyd runs on the router, reads a +local socket, and nothing about the traffic leaves the device. Measured on the +development router, netifyd held 16 sockets over 4 days 23 hours of uptime, 12 +unix and 3 packet-capture, and **zero TCP or UDP sockets**, with no outbound +connection at any point. + +appflow v1 ships two tabs: + +1. **Overview (live)**, the live view, which range-based DPI screens do not + generally provide. Live total + throughput chart, top apps by current rate, category breakdown, top + devices. Poll 5 s. +2. **Statistics (Past hour)**, the range view, from in-RAM buckets + (12 × 5-min per app): Top-10 chart, the Application/Download/Upload/Total + table (sorted, searchable, "All traffic" row), per-app detail drawer with + 12-point time series + per-device rows (name, MAC, last seen, DL/UL, + share %), and a Clear button (`reset`). Poll 15 s. + +**Past day / Past week tabs are phase 2** (require persistence; the UI shows +the tabs disabled with a tooltip, so the seam is visible rather than hidden). + +Deliberate departures from the conventional layout, documented as choices: +- **No app icons in core, by design.** Vendor screens' per-app icons ship + from their CDN and are proprietary (netify/eGloo data). Core + `luci-app-appflow` renders letter-tile avatars colored by category on its + own, zero licensing exposure, still scannable. A separate, optional + package, `luci-app-appflow-icons` (§9), adds brand icons for the ~100 most + recognizable detectable apps; core probes for that package's icon + directory at render time and falls back cleanly when it is not installed. +- **No per-app Block toggle** (vendor screens wire it to a content-filter product; + out of scope for a statistics package). +- **No enable/disable toggle**, that gates a heavy NFQUEUE pipeline elsewhere; appflowd + is a lightweight socket consumer, service control via standard init. +- Device names resolved from `/tmp/dhcp.leases` + netifyd status.json MAC map + (vendor implementations use a proprietary client registry socket). + +## 5. ubus contract (v1 draft) + +Object `appflow`: + +| method | args | returns | +|---|---|---| +| `summary` | - | totals, rates, top_apps[], top_categories[], top_devices[], window meta | +| `apps` | `{sort, limit}` | full per-app aggregate list | +| `devices` | - | per-device aggregates (+resolved names) | +| `app_detail` | `{app}` | per-device + per-hostname breakdown + hour time series for one app | +| `stats` | `{range:"hour"}` | range totals: all/applications[]/top_apps[] with 12×5-min time series (only `hour` in v1; other ranges rejected until phase 2) | +| `flows` | `{limit}` | live flow list (debug/power-user view) | +| `status` | - | daemon health: uptime, flows tracked, events/s, socket state | +| `reset` | - | zero all aggregates/buckets (the "Clear" function; **write-scope ACL**, separate from read grants) | + +All byte values in **bytes**, rates in **bytes/sec** (UI formats). Timestamps +ms epoch. Every response carries `generated_at` + `agent_connected`. + +## 6. Package layout + +``` +luci-app-appflow/ +├── Makefile # LuCI feed style, PKGARCH:=all +├── htdocs/luci-static/resources/view/appflow/{overview,statistics,common}.js +├── root/etc/init.d/appflowd # procd wrapper +├── root/etc/config/appflow # uci: caps, window sizes, top-N +├── root/usr/sbin/appflowd # ucode daemon (shebang /usr/bin/ucode) +├── root/usr/share/rpcd/acl.d/luci-app-appflow.json +├── root/usr/share/luci/menu.d/luci-app-appflow.json +├── po/templates/appflow.pot # string catalog, GENERATED from source +├── tests/protocol-suite.sh # 54 checks, fake agent, no traffic needed +├── tests/hardware-suite.sh # 15 checks, real traffic vs wire truth +├── tests/frontend-suite.js # 31 checks, Node, runs in CI +├── tests/luci-module.js # loads a real LuCI view module under Node +└── docs/DESIGN.md # this file +``` + +`po/` is not hand-maintained. `luci.mk` discovers languages by globbing `po/*` +and generates a `luci-i18n-appflow-` subpackage per directory, so adding +a translation needs no Makefile change. See CONTRIBUTING.md. + +Depends (from `Makefile` `LUCI_DEPENDS`): `netifyd`, `luci-base`, +`ucode-mod-socket`, `ucode-mod-uloop`, `ucode-mod-ubus`, `ucode-mod-uci`, +`ucode-mod-fs`. + +## 7. History extension seams (deliberate, v1 ships them dormant) + +1. **Single aggregation choke point**: every counter delta flows through one + `account(flow, delta_bytes, ts)` function, the only place history taps in. +2. **Time-bucketed accumulators**: aggregates are structured as + `{cur, buckets[]}` from day one, and the buckets are load-bearing in v1, + not a stub: every aggregate (totals, each app, each device, each + category) carries a 12-slot, 5-minute ring covering the trailing hour + (backs the `stats`/`app_detail` time series), and every + per-(app × device) and per-host breakdown carries a coarser 4-slot, + 15-minute ring covering the same trailing window at a quarter of the + memory. History (§9) extends retention and adds persistence of that same + shape; it does not introduce the buckets themselves. +3. **Persistence interface**: `store.flush(buckets)` no-op stub in v1; later + implementations (RAM-rotating file / collectd exec / sqlite on USB) plug in + without touching accounting. Flash-wear rule: never write per-sample to + overlay; flush coarse buckets only. +4. **ubus contract reserves** `history` method name + `since/until` args. + +## 8. Risks / open items + +- ~~UI reference spec~~, landed 2026-08-21. It lived here as section 4 and + now lives in [frontend.md](frontend.md); the numbering below still skips + 4 rather than renumber every cross-reference in this file. +- ~~netifyd conf details~~, landed: multi-client listen socket, `dump_established_flows`, + `flow_stats` cadence config. Purge event confirmed as `flow_purge` (extracted from the netifyd binary, 2026-08-21). +- ~~ucode `publish()` reliability → decides §3 option A/B~~: resolved, §3 + states Option A shipped, proven reliable on 25.12. +- ~~Dual-capture dedup double-counts NAT'd client traffic~~, FIXED 2026-08-24 + (§3.2) by resolving the ambiguity through conntrack rather than guessing. + Client 99.8% and router 97.9% of wire truth, each counted once. Degrades + safely when conntrack is unreadable. Original defect description follows. +- **(historical) The defect this replaced (§3.2).** + netifyd 4.4.7 reports the router's own WAN MAC on the external capture of a + NAT'd flow, so `dev_is_router` is true and `want_shadow` is false: the twin + is counted again under `router`. Measured 163%, 196% and 199% of wire ground + truth across three trials. The per-device row for the client is accurate to + 496 bytes in 53 MB; it is the **grand total** and the `router` row that are + wrong. No fix yet: `!dev_is_router` protects genuinely router-originated + traffic and cannot simply be removed, and `ip_nat` is not a reliable + discriminator. See §3.2 for the fix direction. +- ~~**CT_MAX described as a memory bound**~~, FIXED 2026-08-24, also from the + panel. `ct_load()` called `fs.readfile()` and then `split(raw, " +")`, which + materialised the whole conntrack table and the whole line array *before* + `CT_MAX` was consulted, so the constant bounded the retained map but not peak + allocation, and its own comment calling it a memory bound was wrong. It now + reads a line at a time via `fs.open()` / `read("line")`, so peak is one line + and the cap stops the work as well as the retention. + + A second defect in the same function: the non-forced throttle read + `conntrack.entries && (now - conntrack.at) < CT_TTL_MS`. A snapshot that + parsed to zero rows while the file was readable made that falsy and disabled + the throttle entirely, and the non-forced path has no `CT_FORCE_MIN_MS` floor, + so every external flow identification re-read the whole file. Now gated on + `conntrack.at`, i.e. on having read recently rather than on having read + recently *and* found something. +- **Byte conservation is now instrumented, not just asserted** (2026-08-24, + suggested by a panel juror). `status.bytes` reports `reported` (what netifyd + said moved, before any clamp or shadow decision), `attributed` (what reached + an aggregate), `shadowed` (what was deliberately dropped as a NAT twin) and + `leaked` (the residual, which should be 0). Both silent byte-loss defects this + daemon has had would have been visible here from the first minute. Measured 0 + across every run since. In a NAT-ed transfer `reported` lands at roughly twice + wire truth with about half shadowed, which is the dual capture being + deduplicated, visible for the first time. +- EA8500 resource baseline MEASURED (2026-08-21, light load, 12 flows, + synthetic traffic): netifyd ~16.5 MB VSZ, ~0% CPU, box 95% idle, 397 MB + free. Heavy-load measurement (real client routed through) still pending. +- ~~Byte conservation through the Unknown/late-stub path~~, VERIFIED sound + 2026-08-22 (§2.5), including the sub-`update_interval` case. +- **Connect/reconnect blindness (KNOWN LIMITATION, will ship).** Flows already + established when the daemon or netifyd (re)starts have their remaining bytes + attributed to Unknown, because netifyd 4.4.7 emits no `flow` event for them + (§2.4, §2.5). Bounded to that window, self-healing, non-destructive. Cannot be + fixed without an established-flow dump upstream. Documented in the + README rather than hidden. +- ~~**reshadow() exempted the flows it existed to correct**~~, FOUND by an + adversarial panel and FIXED 2026-08-24. `reshadow()` returned early on + `dev_key == "router"`, which is exactly how a WAN-side capture of a NAT-ed + client flow is classified, because netifyd reports the post-translation + identity (§3.2). The conntrack twin resolution added earlier that day went + into `flow_identify()` and was never wired into `reshadow()`, so the one class + of flow it must correct was the one it skipped. It now calls `ct_nat_twin()` + and defaults the same way `flow_identify()` does. + + Measured on hardware, NAT-ed client transfer with `appflowd` restarted + mid-flight, appflow total against the client veth's own tx counter: + + | run | before fix | after fix | + |---|---|---| + | 1 | 0.701 | 0.892 | + | 2 | 0.791 | 1.013 | + + No-restart control after the fix: 1.012. **A caveat worth keeping:** the panel + predicted a persistent *double* count of the central total. That is not what + was measured. The old code *under*-counted after a restart (0.70-0.79) and the + fix restores accuracy; it does not remove an inflation that was never + observed. The code defect was real, the predicted magnitude and sign were not. + The residual gap in run 1 is connect-blindness, below. +- **Late re-point stranding (OPEN, unquantified).** If netifyd first reports a + flow unclassified (`app_id <= 0`) and only later re-points it via a detection + update, bytes counted before the re-point stay under Unknown, because the + accounting seam is append-only by design (see the re-point comment in + `classify()`). No counter currently exposes this, so its real-world magnitude + is unknown. It could not be induced deliberately on the bench (it needs a + protocol netifyd classifies only after several packets on a stable + connection). Cheapest settling test: a real-traffic socket capture correlating + per-digest `flow`/`flow_stats`/detection-update ordering, excluding + connect-time orphans by `first_seen_at`; alternatively instrument the re-point + in `classify()` to accumulate stranded mass into a `status` counter. Deferred + rather than guessed at. + +## 9. Roadmap (queued, post-v1) + +1. **v1.1, `luci-app-appflow-icons`, a SEPARATE package.** Hand-curated mapping + (netify app id → icon) for the ~100 most-recognizable detectable apps, using + monochrome SVGs from CC0/Apache-licensed collections (simple-icons / + dashboard-icons), bundled locally, tinted by category color. Deliberately + split from core, i18n-style, for legal blast-radius isolation: brand icons + are trademarks regardless of collection license, so if anyone ever objects + the icon package is delisted on its own and core is untouched. Coupling is + runtime-soft in BOTH directions: core probes the icon directory at render + time and falls back to letter-tile avatars; the icon package depends on + nothing and breaks nothing when absent or removed. Same repo, second + package directory. +2. **v2, Past day / Past week ranges** (persistence per §7 seams; flash-wear + policy: coarse-bucket flushes only, tmpfs-first with periodic backup, + tiered downsampling). + +## 10. As-implemented contract notes (v1 final) + +§5 sketched the method set; the implemented field naming is authoritative: +live aggregate rows use `bytes_up/bytes_down/bytes_total` + +`rate_up/rate_down/rate_total` (+ `key/name/tag/category`), while the +The `stats`/`app_detail` range views use `download/upload/total`, which is the +conventional vocabulary for that screen. `status` is nested +(`socket.*`, `flows.*`, `aggregates.*`, `agent.*`, `memory.*`). +`app_detail` takes the aggregate `key` or the raw tag or the display +label (resolver tries all three). The frontend ships normalizers for +both dialects in `view/appflow/common.js`. luci-lib-chartjs was dropped: +the packaged build is a Chart.js 1.x fragment (Doughnut/Pie only), and +inline SVG follows LuCI theme variables, which canvas cannot. + +## 11. AI service breakout + +### 11.1 Why netifyd cannot do this + +Measured on two routers, 2026-08-27. `/etc/netify.d/netify-apps.conf` is the +Netify Agent's signature set. On both it is dated **10 August 2023**, carries +**199** application signatures, and contains **zero** entries matching +anthropic, claude, openai, chatgpt, huggingface, perplexity, mistral, copilot, +bard or gemini. + +So AI traffic arrives with `detected_application` 0 and aggregates into generic +HTTP/S alongside everything else. This is vendor data three years stale, not a +configuration problem, and nothing on the router can change it. + +### 11.2 Why appflow can + +netifyd already reports the TLS SNI as `host_server_name`, and appflow already +stores it. Traffic generated to the real endpoints on the bench carried +`api.anthropic.com`, `claude.ai` and `api.openai.com` with no code change at +all. The identifying data was there the whole time; nothing was making use of +it. + +The daemon is the right layer because aggregation happens there. The browser +only ever receives summarized rows, so a frontend fix would arrive after the +decision that matters had already been made. + +### 11.3 What it does + +In `flow_identify()`, after netifyd's identity is resolved and after the +category is derived, the SNI is matched by **domain suffix** against a built-in +table. On a hit the application key, label, tag and category are replaced. + +``` +key ai:anthropic-claude +name Anthropic (Claude) +tag appflow.anthropic-claude +cat ai-assistants +``` + +Four categories: `ai-assistants`, `ai-media`, `ai-developer`, +`ai-infrastructure`. The last covers aggregators, inference APIs, GPU rental +and vector stores, which are different businesses but one thing from a +network's point of view. + +**The category is a SLUG on the wire, and 1.1.0 got this wrong.** It shipped +`AI assistants` and its three siblings as display strings, which read correctly +in English and could therefore never be translated: the browser renders a +category by looking the slug up in a table of `_()` literals, and a string that +arrives already rendered has no key to look up. Every netifyd category travels +as a lowercase slug; these four were the only ones that did not, and they were +ours. Corrected in 1.1.1. `frontend-suite.js` now reads this table out of the +daemon source and fails if a category is emitted that the frontend cannot +translate, so the two halves cannot drift apart again. + +**Keyed on the vendor label, not the matched domain.** `anthropic.com` and +`claude.ai` are one vendor and must aggregate into one row; keying on the +suffix would produce two identically-named rows and split the total. + +**The `appflow.` tag prefix is deliberate.** netifyd's own tags are `netify.*`. +Nothing here pretends to have come from netifyd. + +### 11.4 The gate, and why it makes the feature self-retiring + +**netifyd's own answer wins.** The override runs only when +`detected_application` is 0, with one exception: entries flagged `strong`, +which override even a positive identification. + +`strong` is set only where netifyd is known to recognize the PARENT brand and +would otherwise swallow the AI service inside it: Gemini reported as Google, +Copilot as GitHub, Qwen as Alibaba. Everywhere else the rule is conservative, +which means **the day Netify ship AI signatures this table falls silent by +itself** rather than permanently shadowing better data. + +### 11.5 Matching is anchored on label boundaries + +The SNI is lowercased, a trailing dot is stripped, and the last two, three and +four labels are rebuilt and looked up in turn. Never a substring test. + +`anthropic.com.attacker.example` is a hostname anyone can register and point +anywhere. A substring match on `anthropic.com` matches it. Rebuilding the last +N labels makes that impossible by construction rather than by care, and the +suite asserts it with a flow carrying exactly that hostname. + +### 11.6 Accounting is untouched + +This changes an identity, never a byte. `account()` never sees it, byte +conservation is exactly as it was, and the existing conservation checks still +hold. `tests/protocol-suite.sh ai` asserts a hand-computed total across a batch +containing AI flows for precisely this reason. + +### 11.7 Limitations, stated rather than discovered + +- **This is hostname matching, not DPI.** It asserts a classification netifyd + did not make. The technique is legitimate, matching the SNI is what netifyd + itself does for HTTP/S, but the accurate description is "we recognize the + name", never "we inspected the protocol". +- **The list rots.** New services appear constantly and nothing here updates + itself. `option ai_breakout '0'` turns it off entirely. +- **ECH would end it.** Encrypted Client Hello encrypts the SNI. netifyd has + the identical exposure so this is no worse than the status quo, but it is not + durable and should not be sold as such. +- **Region-encoded hostnames under a generic cloud domain cannot be matched.** + `bedrock-runtime.us-east-1.amazonaws.com` has no suffix specific to Bedrock, + and matching `amazonaws.com` would label every S3 bucket on the network as + AI. Deliberately absent rather than approximated. +- **A service behind a shared CDN hostname is invisible**, because the SNI + names the CDN. +- **No icons ship for these entries.** They render as letter tiles until the + icon pack gains matching `appflow.*` keys. + +## 12. Extensibility: the hostname table is not a constant + +The AI breakout (section 11) shipped its table as a constant in the daemon, +which meant adding a service required editing the source and rebuilding the +package. For a tool whose central claim is that netifyd's signature set is three +years stale, being un-extendable is the wrong shape, and the question that +exposed it was the obvious one: "I have a service I want measured, what do I +do?" + +`config hostmap` sections in `/etc/config/appflow` are read at config load and +consulted BEFORE the built-in table, so one mechanism does both jobs: adding a +service that is missing, and correcting one that is wrong. + +``` +config hostmap + option suffix 'torproject.org' + option name 'Tor' + option category 'Privacy' + option strong '0' +``` + +**A malformed entry is refused and named in the log.** A suffix with no dot can +never match, because the matcher rebuilds whole labels rather than comparing +substrings, so accepting one produces a rule that silently does nothing and +looks identical to the feature being broken. + +That guard has no effect on classification, only on whether the user is told, +and the test for it had to be written accordingly. Two earlier versions were +tautologies: one grepped the whole log and passed forever once the message had +ever appeared, the next counted log lines and read past the count, which failed +because `logread` is a RING whose line count stops growing once it is full. A +unique marker written with `logger` is immune to both. See +`tests/protocol-suite.sh hostmap`. + +## 13. Shipped in 1.1.2: the Router row had never counted a byte + +**The most serious defect this package has had, and it was invisible by +construction.** On both routers checked, the `router` pseudo-device reported +exactly zero bytes. Production had logged **107,476** flows where conntrack +positively confirmed the router originated the traffic, against a device row +reading zero for 2.6 days and 52 GB. The dashboard correctly hides a zero-byte +device, so the feature was absent rather than visibly broken, and nobody +noticed for the life of the package. + +**The cause is one line and it is worth stating precisely.** `ct_nat_twin()` +built its lookup key from the event it was handed: + + other_ip | local_ip | other_port | local_port + +netifyd puts the ports on the initial `flow` event and on nothing afterwards. A +`flow_stats` payload carries addresses and byte counters only. So the key built +from an update had two empty fields, matched nothing, and returned `null`. +`reshadow()` treats `null` as grounds to discard, so every router-originated +flow was shadowed on its first stats update, before a single byte reached the +aggregate. `flow_identify()` resolved it correctly, once, and the next event +threw the answer away. + +The fix has two halves, and they are independently sufficient, which is why the +tests distinguish them: + +- **The ports are stored on the flow record** and the lookup prefers them. This + is the root cause and the check on `conntrack.unresolved` isolates it. +- **A positive finding is remembered.** `fr.ct_local` records that conntrack + once said the router originated this flow, and a later lookup that cannot + answer no longer overturns it. Only a positive "this is a NAT twin" may. + Absent evidence is not positive evidence, the same rule §5 already states + about `statusStrip()`. + +**The dedup is deliberately unchanged.** A fix that simply stopped shadowing +would restore the 163-199% over-count this mechanism exists to prevent, so the +suite asserts that a router flow conntrack cannot confirm is still discarded. + +`conntrack_path` became a config option purely so this is testable, exactly as +`socket_path` already was. The `router` group in `protocol-suite.sh` supplies a +conntrack fixture and drives the real daemon. + +**Also in 1.1.2: tile letters outside ASCII.** `tile()` stripped with +`/^[^0-9A-Za-z]+/`, which consumes a name written in any non-Latin script +entirely and falls through to `?`. Every Chinese, Japanese, Korean, Russian, +Greek, Arabic and Hebrew name rendered an identical question mark. Not a +translation bug: any LAN client with a non-Latin DHCP hostname hit it on every +release. Now stripped on `\p{L}\p{N}` with the `/u` flag. + +## 14. Shipped in 1.1.1: labels that could not be translated + +Every item that stood in this section as "queued for 1.1.1" shipped in 1.1.0 +instead (commit `b8ca31d`): the search placeholder, exclude terms, and the +clickable donut. The section was left describing them as pending for three +weeks. What actually became 1.1.1 is below. + +**A label is only translatable if a literal reaches `_()`.** That is what the +`.pot` extractor scans for, and three separate paths produced labels that never +did. All three were invisible to a green test run, and all three were reported, +directly or indirectly, by a first-time contributor's translation pull request. + +**`catLabel()` held 12 entries and title-cased the rest at runtime.** That is +string manipulation, not translation: `web` became `Web` by algorithm, in +English, in every language. The live netifyd category file carries 43 tags +across its application and protocol indexes. The table now holds all of them. + +**The daemon names its three synthetic pseudo-devices in English**, because a +daemon has no idea what language a browser wants. The frontend now maps the +stable key it sends, not the name, so a daemon-side rewording cannot silently +revert the translation. + +**The AI categories were ours, not inherited.** See section 12. + +The interesting part is why no test caught it. `frontend-suite.js` asserted +that `catLabel('networking')` returns `'Networking'`, which is satisfied +identically by a `_()` lookup and by a title-caser, and it asserted that every +string reaching `_()` is in the catalogue, which is blind by construction to a +string that never reaches `_()`. The new group asserts **membership in the set +of strings the module actually passed to `_()`**, recorded by the harness as it +loads. Six product edits were executed against it to confirm each check can +fail; two of those runs found defects in the test rather than the product. + +## 15. Filtering and the drill-down + +### 14.1 Where the logic lives, and why + +`parseFilter()` and `matchFilter()` are in `common.js`, not inside either +view's render function. Both views share them so the two cannot drift, and Node +can load `common.js`, so the filter is covered by `tests/frontend-suite.js` +rather than by looking at a browser. The sibling package learned the same +lesson the same week: logic that decides something belongs where a test can +reach it. + +Terms are space separated and every one must pass, so terms narrow. A leading +minus excludes. Exclusion is not decoration: the question that produced this +feature was "one streaming session is burying everything else", and an +include-only filter cannot express it, because you would have to already know +what you wanted to keep. + +A bare `-` is dropped rather than treated as exclude-all, because that is what +a half-typed term looks like and blanking the table between two keystrokes is +not acceptable. + +### 14.2 Fetching past the top-N cut + +The overview asks the daemon for `FETCH_LIMIT` (50) application rows and +displays `TOP_APPS` (8). The extra rows are the filter's raw material. Without +them, filtering a top-10 list to "AI" returns nothing the moment a streaming +session pushes AI out of the top 10, which looks broken and is worse than +having no filter at all. + +### 14.3 The drill-down needs no new aggregate + +Both directions are one pass over the live flow table: + + devices_for_apps(keys) which devices carry these applications + apps_for_device(dev) which applications is this device using + +Every flow record already carries `dev_key`, `app_key` and `cat_key`, so the +join exists on every flow, and the table is already bounded by `flow_max`. No +new storage. + +**This was first answered wrongly.** The initial response was that it required +a per-device-per-application aggregate and was therefore a bounded-memory +design question. Checking the flow record disproved that in two minutes. The +episode is recorded because the shape of the mistake matters more than the +mistake: an architectural-sounding objection offered instead of a two-minute +check. + +### 14.4 Attributed bytes, not lifetime counters + +Both functions sum `fr.up + fr.down`, the bytes appflow attributed, and never +`fr.total`, which is netifyd's lifetime counter for the connection. A stub flow +adopted mid-transfer has a large lifetime and small attributed bytes, so the +two differ exactly where it matters. + +The test fixture carries one flow with a lifetime of 9000 against 1000 +attributed, for one reason: without it every flow had the two equal, summing +the wrong field was invisible, and the sabotage for that check passed against +broken code. + +### 14.5 Three bugs of one shape + +Getting this working produced three defects that presented identically: wired +up, no error, no effect. + +- **`null` for a string ubus argument.** ubus rejects it with code 2 rather + than treating it as absent, and `L.resolveDefault` swallowed the rejection. +- **The poll repainted over the filtered result.** A side call filtered the + device card and the next five-second poll overwrote it. Filtering is view + state, so every fetch carries it rather than one special path. +- **`normDevice` dropped the daemon's `key`.** Clicking a device passed + `undefined`, which *cleared* the selection instead of setting it. + +None of these are catchable by a test of logic, because in each case the logic +was right and the wire was not connected. They were found by driving the page. +That is the limit of this package's test strategy and it is written here +rather than left implicit. + +### 14.6 One ACL grant was widened, deliberately + +`apps` was unbound in the frontend and ungranted in the ACL after an August +security review. The device drill-down needs it, so it was bound and granted, +in that order, exactly as that review's own note required. + +`apps {device}` returns an aggregate per device and application from the live +flow table. It is more revealing than either list alone, since it says which +device used which application, and it is still far short of `flows`: no remote +address, no port, no per-connection record. **`flows` remains unbound and +ungranted.** diff --git a/luci-app-appflow/docs/ai-filter.png b/luci-app-appflow/docs/ai-filter.png new file mode 100644 index 000000000..1062727af Binary files /dev/null and b/luci-app-appflow/docs/ai-filter.png differ diff --git a/luci-app-appflow/docs/frontend.md b/luci-app-appflow/docs/frontend.md new file mode 100644 index 000000000..e4e0d1b34 --- /dev/null +++ b/luci-app-appflow/docs/frontend.md @@ -0,0 +1,11 @@ +# AppFlow frontend + +- `common.js`: shared class (`require view.appflow.common as appflow`); ubus bindings, payload normalizers, byte/rate formatting, category colors, the icon probe, SVG chart primitives, injected CSS. +- `overview.js`: Overview (live). Polls `summary`+`devices`+`status` every 5 s; the 60-sample chart window lives in the view, not the daemon. +- `statistics.js`: Statistics (past hour). Polls `stats {range:"hour"}` every 15 s; search, sort and the detail drawer are client-side; `reset` sits behind a confirm dialog. +- Menu: `root/usr/share/luci/menu.d/luci-app-appflow.json`, top-level `admin/appflow` firstchild node gated on the `luci-app-appflow` ACL group. + +Conventions: charts are hand-drawn SVG (`lineChart`/`columnChart`/`donut`) so they inherit the theme's CSS variables and read on both bootstrap themes; `luci-lib-chartjs` is unused (its shipped build ships Doughnut/Pie only, no Line/Bar). All CSS is prefixed `af-` and injected once via `appflow.injectCSS()`. +Every payload field is read through `normApp`/`normDevice`/`normTotals`/`normSeries`, which absorb the daemon's two dialects (`bytes_down`/`rate_down` on live aggregates, `download`/`upload` on range views); tighten those alias lists once the DESIGN §5 contract freezes instead of reaching into payloads from the views. +Icons are an optional soft dependency (DESIGN §9): `loadIcons()` probes `/luci-static/resources/appflow-icons/manifest.json` once per page load from each view's `load()`, keyed by the row's netify `tag`; if the sibling package is absent the probe resolves silently and every tile stays a letter tile. `simple-icons` entries are fill-less glyphs and get whitened over the category-colored tile; other sources render as-is. Category colors come from the fixed `CATEGORY_SLOT` table (hash fallback) so the doughnut legend never shows two identical swatches. +Both views build their skeleton once and repaint only inner nodes, so the search box keeps focus and caret across polls; each keeps a `fails` counter so one dropped poll does not flash the "appflowd is not running" screen. diff --git a/luci-app-appflow/docs/netifyd-4.4.7-interface.md b/luci-app-appflow/docs/netifyd-4.4.7-interface.md new file mode 100644 index 000000000..cd94c660b --- /dev/null +++ b/luci-app-appflow/docs/netifyd-4.4.7-interface.md @@ -0,0 +1,624 @@ +# The netifyd 4.4.7 JSON export interface, as shipped by OpenWrt + +## Scope + +This document describes the JSON socket interface exposed by **netifyd 4.4.7**, +the exact version OpenWrt packages and installs (`netifyd-4.4.7-r2.apk` at the +time of writing). It does **not** describe the current upstream Netify Agent +(v5.x), which has moved to a plugin (`plugins.d`) architecture with a +different sink model, additional fields (`digest_prev`, `risks.*` extensions, +`app_ip_override`), and options that do not exist in 4.4.7. Netify's own +public documentation site currently documents v5, which is a meaningful +source of confusion if you go looking for confirmation of anything here: most +of what you'll find online describes a daemon that is not the one running on +your router. + +Everything stated as fact below is grounded in one of two sources: + +1. **Direct inspection of the 4.4.7 source and OpenWrt packaging** + (`gitlab.com/netify.ai/public/netify-agent` at tag `v4.4.7`, and the + `openwrt/packages` build recipe). +2. **A real captured stream from a running 4.4.7 instance** on a Linksys + EA8500 (OpenWrt 25.12.5, `arm_cortex-a15_neon-vfpv4`), taken over a UNIX + socket connection immediately following daemon start. Excerpts from that + capture are reproduced verbatim throughout this document (only field + *values* like MAC/IP addresses and timestamps are real lab data, not + synthetic). + +Where the two disagree, or where a claim could only be confirmed from source +and not observed live, this is called out explicitly. An "Unverified / +unknown" section at the end collects every open question directly rather +than papering over it. + +**A note on `dump_established_flows`.** You may see this option referenced in +blog posts, forum threads, or even in comments in older netifyd +configuration examples. It does not exist in 4.4.7: the string is entirely +absent from the compiled binary and from the 4.4.7 man page source. It is a +v5-era `plugins.d` option. Practically, this means a client that connects to +the socket mid-stream (which, for most consumers, is every connection) gets +**no backfill of already-tracked flows**. It starts blind. See "Connection +lifecycle" and "Practical consumer guidance" below for what this means and +how to handle it; the real capture used throughout this document directly +demonstrates the resulting behavior. + +## 1. Socket and wire framing + +netifyd exposes a UNIX domain socket, configured in `/etc/netifyd.conf`: + +```ini +[socket] +listen_path[0] = /var/run/netifyd/netifyd.sock +``` + +On stock OpenWrt this is active by default as soon as the `netifyd` init +script starts the daemon; no additional configuration is required to get a +live stream. (A TCP listener is also supported via `listen_address[n]` / +`listen_port[n]`, but is not used by default on OpenWrt and was not +exercised for this document.) + +**Wire format**: every message is a JSON object terminated by `\n`, and every +payload line is preceded by a one-line header of the form `{"length": N}`. +This is newline-delimited JSON with an explicit length prefix, not bare NDJSON. + +Real captured pair (the very first two lines netifyd writes to a freshly +connected socket): + +``` +{"length": 165} +{"agent_version":"4.4.7","build_version":"Netify Agent/4.4.7 (openwrt; arm; conntrack; netlink; dns-cache; plugins; regex)","json_version":1.9,"type":"agent_hello"} +``` + +**Framing detail confirmed by direct measurement**: `N` is the byte length of +the payload line **including its trailing `\n`**, not the length of the raw +JSON content alone. Checked across all 17 header/payload pairs in the capture +used for this document, the declared `length` matched `byteLength(payload) + 1` +in every case, and never matched `byteLength(payload)` alone. A parser must +account for the newline when deciding how many bytes to read for a given +frame (or, more robustly, simply read line-delimited text and treat the +length header as a frame-boundary hint rather than the sole read-size +authority). + +Parser rule, in short: read a line; if its only key is `length`, it is a +frame header, discard it; otherwise the line is a complete JSON event and its +`type` field determines how to interpret it. + +## 2. Connection lifecycle + +The real capture shows this exact sequence on connect (daemon `update_interval` +was 15 seconds for this run): + +1. `agent_hello` -- sent once, immediately, before anything else. +2. `agent_status` -- sent immediately after `agent_hello`. This first status + frame is a **reduced** one; see the field-presence note in section 3.3. +3. `definitions` -- sent once per connection, a large one-time dump of the + application and protocol id-to-name tables (416 applications / 217 + protocols in this capture). +4. A burst of `flow_stats` events (11 of them in this capture) for flows that + were **already being tracked** before this client connected. **None of + these digests were ever announced via a `flow` event on this connection.** + This is the live, directly observable consequence of + `dump_established_flows` not existing in 4.4.7: the client is receiving + counter updates for flows whose identity (application, protocol, hostname, + IPs) it was never told. +5. A second `agent_status`, 15 seconds after the first (matching + `update_interval`), this time carrying the fuller field set (interfaces, + devices, per-interface capture stats). +6. Two `flow` events, for newly detected DNS queries to `openwrt.org`, + showing the daemon has now genuinely observed new traffic from + first packet. + +No `flow_purge` event occurred during this capture window (the two new flows +were still active when the capture ended). Its shape is documented in +section 3.6, confirmed live via 10+ purge events captured separately during +this project's hardware testing (`DESIGN.md`), even though none happened to +land in the specific capture excerpted in this document. + +Practical implication for a long-running consumer (this is what +`appflowd` in this repo does): on every connect or reconnect, assume you know +nothing about flows already in progress. You will start receiving +`flow_stats` (and eventually `flow_purge`) for digests you have never seen a +`flow` event for. Treat that as the normal, permanent case, not a transient +startup race. + +## 3. Event type reference + +Every event on the socket, except `agent_hello` and `definitions`, shares the +same top-level envelope: + +```json +{"type": "", "interface": "", "internal": , "established": , "flow": { ... }} +``` + +`interface` is the capturing interface name (`br-lan`, `wan`, etc.). `internal` +reflects which capture point saw the packet (true for internal/LAN-side +capture, false for external/WAN-side), which matters because netifyd watches +both `br-lan` and `wan` simultaneously and a NAT'd client's traffic can appear +as two distinct flows with two distinct digests (see `DESIGN.md` section 3.2 +for this project's dedup policy; that is an application-level concern, not +part of the wire protocol itself). + +`established` is present on every `flow`/`flow_stats` event in the capture +and was `false` in all observed instances. Its precise semantics were not +confirmed from source in the research this document is based on, and the +capture is too short and too uniform to infer it empirically; see +"Unverified / unknown." + +### 3.1 `agent_hello` + +Sent once, immediately on connect, before any other event. + +```json +{"agent_version":"4.4.7","build_version":"Netify Agent/4.4.7 (openwrt; arm; conntrack; netlink; dns-cache; plugins; regex)","json_version":1.9,"type":"agent_hello"} +``` + +| Field | Type | Notes | +|---|---|---| +| `agent_version` | string | e.g. `"4.4.7"` | +| `build_version` | string | Human-readable build string; the parenthesized list is compile-time capability flags. This build has `conntrack` compiled in (relevant to `ct_id`/`ct_mark`, see 3.4). | +| `json_version` | number | Schema/protocol version for the JSON payloads on this socket, `1.9` in this capture. Matches the `version` field later seen in `agent_status`. | +| `type` | string | Always `"agent_hello"` | + +### 3.2 `definitions` + +Sent once per connection, immediately after the first `agent_status`. Carries +the full id-to-name tables for both id spaces used elsewhere in the stream +(`detected_protocol` and `detected_application`). This is a large message +(18,704 bytes in this capture, framed with the `{"length": N}` header like +everything else). + +Truncated real excerpt (full arrays omitted for length; see section 5 for a +detailed discussion of what's in them): + +```json +{"applications":[{"id":248,"tag":"ZOOM"},{"id":177,"tag":"ZMQ"}, ... ,{"id":10126,"tag":"netify.zendesk"}], + "protocols":[{"id":248,"tag":"ZOOM"},{"id":177,"tag":"ZMQ"}, ... ,{"id":191,"tag":"OOKLA"}], + "type":"definitions"} +``` + +| Field | Type | Notes | +|---|---|---| +| `applications` | array of `{id: uint, tag: string}` | 416 entries in this capture. Resolves `flow.detected_application`. **Do not** also use this array to resolve `detected_protocol` -- see the id-collision warning in section 5. | +| `protocols` | array of `{id: uint, tag: string}` | 217 entries in this capture. Resolves `flow.detected_protocol` only. | +| `type` | string | Always `"definitions"` | + +The captured counts (416 / 217) match `DESIGN.md`'s figures exactly, which is +good independent corroboration that this capture is representative and not a +truncated or unusual snapshot. + +### 3.3 `agent_status` + +Agent-level telemetry: uptime, CPU, memory, flow-table size, sink (upload) +state, and (on richer frames) per-interface identity and packet-capture +counters. **Never contains per-flow data.** This is the same payload also +written periodically to `/var/run/netifyd/status.json`; on the socket it is +additionally interleaved with flow events, once per `update_interval`. + +**Two different shapes were observed for this event type in the same +capture, and this is worth flagging explicitly because it is easy to design +a parser around only the richer one and then break on the first frame.** + +First `agent_status` (sent immediately after `agent_hello`, before +`definitions`): + +```json +{"cpu_cores":2,"cpu_system":0.075803,"cpu_system_prev":0.028308,"cpu_user":1.212848,"cpu_user_prev":0.113235,"dhc_size":1,"dhc_status":true,"flow_count":16,"flow_count_prev":0,"maxrss_kb":15372,"maxrss_kb_prev":9332,"sink_status":false,"sink_uploads":false,"timestamp":1787325073,"type":"agent_status","update_imf":1,"update_interval":15,"uptime":15,"version":1.9} +``` + +Second `agent_status` (15 seconds later, on the regular cadence): + +```json +{"agent_version":"4.4.7","cpu_cores":2,"cpu_system":0.077808,"cpu_system_prev":0.075803,"cpu_user":1.256057,"cpu_user_prev":1.212848,"devices":{"b4:2e:99:3a:d6:e4":["192.168.1.10"],"c0:56:27:4e:3e:92":["192.168.72.10"],"c0:56:27:4e:3e:93":["192.168.1.1"],"dc:b5:4f:ad:0d:6e":["192.168.72.21","fe80::18f3:bf8d:dc6e:980f"]},"dhc_size":1,"dhc_status":true,"flow_count":21,"flow_count_prev":16,"interfaces":{"br-lan":{"addr":["fe80::c256:27ff:fe4e:3e93","fda8:3a0e:1478::1","192.168.1.1"],"mac":"c0:56:27:4e:3e:93","role":"LAN","state":1},"wan":{"addr":["fe80::c256:27ff:fe4e:3e92","192.168.72.10"],"mac":"c0:56:27:4e:3e:92","role":"WAN","state":1}},"maxrss_kb":15372,"maxrss_kb_prev":15372,"sink_status":false,"sink_uploads":false,"stats":{"br-lan":{"capture_dropped":0,"capture_filtered":0,"discarded":0,"discarded_bytes":0,"ethernet":35,"fragmented":0,"icmp":0,"igmp":0,"ip":35,"ip_bytes":10806,"largest_bytes":1718,"mpls":0,"pcap_drop":0,"pcap_ifdrop":0,"pcap_recv":35,"pppoe":0,"queue_dropped":0,"raw":35,"tcp":35,"tcp_resets":0,"tcp_seq_error":0,"udp":0,"vlan":0,"wire_bytes":11646},"wan":{"capture_dropped":0,"capture_filtered":0,"discarded":2,"discarded_bytes":102,"ethernet":158,"fragmented":0,"icmp":2,"igmp":0,"ip":156,"ip_bytes":149662,"largest_bytes":4338,"mpls":0,"pcap_drop":0,"pcap_ifdrop":0,"pcap_recv":158,"pppoe":0,"queue_dropped":0,"raw":158,"tcp":134,"tcp_resets":2,"tcp_seq_error":0,"udp":20,"vlan":0,"wire_bytes":153406}},"timestamp":1787325074,"type":"agent_status","update_imf":1,"update_interval":15,"uptime":30,"version":1.9} +``` + +| Field | Type | Present on | Notes | +|---|---|---|---| +| `type` | string | both | Always `"agent_status"` | +| `version` | number | both | JSON schema version, `1.9` here (same value as `agent_hello.json_version`) | +| `timestamp` | uint | both | Epoch **seconds**. See the timing anomaly noted below; don't lean on this for interval math. | +| `uptime` | uint | both | Seconds since agent start. Reliable for computing elapsed time between status frames. | +| `update_interval` | uint | both | Configured status/stats cadence in seconds, `15` here | +| `update_imf` | uint | both | Present, value `1` in both frames. Meaning not confirmed by any source consulted; see "Unverified / unknown." | +| `cpu_cores` | uint | both | | +| `cpu_user`, `cpu_user_prev`, `cpu_system`, `cpu_system_prev` | number (seconds) | both | Cumulative CPU time, current and previous sample | +| `flow_count`, `flow_count_prev` | uint | both | Size of the daemon's live flow table, current and previous sample. This is a **count**, never the flows themselves. | +| `maxrss_kb`, `maxrss_kb_prev` | uint | both | Resident memory high-water mark, current and previous sample | +| `dhc_status` | bool | both | DNS hint cache active | +| `dhc_size` | uint | both | DNS hint cache entry count | +| `sink_status` | bool | both | Cloud upload sink connected. `false` throughout this capture (sink disabled). | +| `sink_uploads` | bool | both | Whether upload is compiled/enabled at all | +| `agent_version` | string | second frame only | e.g. `"4.4.7"` | +| `devices` | object, MAC string to array of IP strings | second frame only | MAC to IP(s) map; useful for device-name resolution independent of DHCP leases | +| `interfaces` | object keyed by ifname | second frame only | Per-interface `addr[]`, `mac`, `role` (`"LAN"`/`"WAN"`), `state` (observed `1`, presumed up/down) | +| `stats` | object keyed by ifname | second frame only | Per-interface packet-capture counters (drops, protocol breakdown, byte totals). Not per-flow. | + +**Consumer guidance from this asymmetry**: do not assume `agent_version`, +`devices`, `interfaces`, or `stats` are present on every `agent_status` +frame. Merge additively into whatever state you're building rather than +replacing it wholesale on frames that omit these keys, or you will discard +good state on the first frame after every reconnect. + +**Timing anomaly, flagged rather than explained**: between the two captured +`agent_status` frames, `uptime` advanced by exactly 15 seconds (matching +`update_interval`, as expected), but the wall-clock `timestamp` field only +advanced by 1 second (`1787325073` to `1787325074`). This is inconsistent +with both fields tracking real elapsed time. It may be a genuine daemon +quirk, or an artifact of how this particular lab capture was recorded (the +JSON was captured live but the header rows were relatively hand-timed for +this exercise). No independent source was available to confirm which. +**Do not use `timestamp` deltas to infer the update cadence; use `uptime` +deltas or the configured `update_interval` instead.** + +### 3.4 `flow` + +Fires when a flow's Layer-7 identity is (re)determined: first classification, +or a later refinement flagged via `detection_updated`. Carries full identity +metadata. **Carries no byte or packet counters** -- confirmed directly by +this capture, where neither of the two captured `flow` events has +`local_bytes`, `other_bytes`, `total_bytes`, or any packet counter field. +Counters arrive separately via `flow_stats`. + +Real captured example (a router-originated DNS query, unedited beyond +formatting): + +```json +{"established":false,"flow":{"category":{"application":31,"domain":0,"protocol":14},"detected_application":10465,"detected_application_name":"netify.openwrt","detected_protocol":5,"detected_protocol_name":"DNS","detection_guessed":false,"detection_updated":false,"dhc_hit":false,"digest":"9c2d7b56c4571e146f65b0d15c12cec661d56b3f","first_seen_at":1787325075857,"first_update_at":1787325075857,"host_server_name":"openwrt.org","ip_nat":false,"ip_protocol":17,"ip_version":4,"last_seen_at":1787325075869,"local_ip":"192.168.72.1","local_mac":"94:83:c4:a8:8f:f1","local_origin":false,"local_port":53,"other_ip":"192.168.72.10","other_mac":"c0:56:27:4e:3e:92","other_port":57052,"other_type":"local","risks":{"ndpi_risk_score":10,"ndpi_risk_score_client":5,"ndpi_risk_score_server":5,"risks":[46]},"soft_dissector":false,"vlan_id":0},"interface":"wan","internal":false,"type":"flow"} +``` + +Reading this example concretely: `local_ip` (192.168.72.1, port 53) is the +upstream DNS resolver; `other_ip` (192.168.72.10, port 57052) is this +router's own WAN address. `local_origin: false` means the "local" side (the +DNS resolver, from netifyd's point of view) did not initiate; the "other" +side (the router) did -- i.e. this is the router itself making a DNS query, +captured on the `wan` interface (`internal: false`). `other_type: "local"` +classifies the *other* endpoint's address as being on a locally-adjacent +network, independent of which capture point (`internal`/`interface`) saw the +packet. This distinction (address locality vs. capture-point locality) is +exactly what `DESIGN.md` section 3.2's router-origin-traffic attribution +policy depends on. + +| Field | Type | Notes | +|---|---|---| +| `digest` | string | 40-char lowercase hex (SHA1). Correlation key; unconditionally present on every event type that carries a `flow` object. | +| `first_seen_at`, `last_seen_at` | uint64 | Epoch **milliseconds** | +| `first_update_at` | uint64 | Present in this capture, not documented in prior source-only notes on this interface. In both captured examples it is numerically identical to `first_seen_at`. Whether it diverges once `detection_updated` fires was not observable in this capture (neither example had `detection_updated: true`). See "Unverified / unknown." | +| `detection_guessed` | bool | | +| `detection_updated` | bool | `false` in both captured examples (both are first-detection events, not re-detections) | +| `dhc_hit` | bool | DNS hint cache was used for this detection | +| `soft_dissector` | bool | | +| `ip_version` | uint | 4 or 6 | +| `ip_protocol` | uint | IANA protocol number (`17` = UDP in the example) | +| `vlan_id` | uint | | +| `ip_nat` | bool | `false` in the example | +| `other_type` | string | Observed value: `"local"`. Source also documents `"remote"`, `"broadcast"`, `"multicast"`, `"unsupported"`, `"error"`, `"unknown"` -- not independently confirmed live in this capture. | +| `local_origin` | bool | Which side initiated; see worked example above | +| `local_mac` / `other_mac` | string | MAC addresses | +| `local_ip` / `other_ip` | string | | +| `local_port` / `other_port` | uint | | +| `detected_protocol` | uint | Id into the `definitions.protocols[]` table | +| `detected_protocol_name` | string | Plain string, not namespaced (`"DNS"` here) | +| `detected_application` | uint | Id into the `definitions.applications[]` table | +| `detected_application_name` | string | `"netify."` format when an app-level identification was made. Source indicates this can be absent/empty when only a protocol, not a specific app, was identified; not observed empty in this capture (both examples had it set). | +| `category.application`, `category.protocol`, `category.domain` | uint | Category ids. Resolved via `/etc/netify.d/netify-categories.json`'s index tables, **not** via the `definitions` applications/protocols name arrays (different id space entirely; see section 5). | +| `dns_host_name` | string | Not present in either captured example (both flows already had `host_server_name` populated from the query itself) | +| `host_server_name` | string | TLS SNI or DNS/HTTP hostname, when available. `"openwrt.org"` in the example. | +| `http.user_agent`, `http.url` | -- | Not observed (neither captured flow is HTTP); documented from source only | +| `dhcp.fingerprint` | -- | Not observed; documented from source only | +| `gtp.*` | -- | Not observed; documented from source only, GTP tunnel inner addressing | +| `risks` | object | Present in both captured examples: `ndpi_risk_score`, `ndpi_risk_score_client`, `ndpi_risk_score_server` (uint scores) and `risks` (array of numeric risk-flag ids, e.g. `[46]`). Not present in the source-derived field list this document's predecessor research was built from -- this is a real, confirmed field that prior source-reading missed. No id-to-name mapping for the values in `risks[]` was found in any source consulted here; see "Unverified / unknown." | +| `ct_id`, `ct_mark` | -- | **Not present** in either captured `flow` example, despite this build having `conntrack` compiled in (per the `agent_hello.build_version` string). Whether this is because these are UDP/no-NAT flows, or because emitting these fields needs an encode flag not set by default, is unconfirmed. | + +Notably absent from `flow` in this capture: `detection_packets` and every +byte/packet counter. Those appear only in `flow_stats` (below), confirming +the two-event split cleanly. + +### 3.5 `flow_stats` + +Periodic per-flow counter update, once per `update_interval`, for flows past +initial detection. No identity metadata (app/protocol names, hostnames, +etc.) -- only the digest, timestamp, and counters. + +Real captured example: + +```json +{"established":false,"flow":{"detection_packets":16,"digest":"f663f9df9734cbaf815438f6e4b7361794729816","last_seen_at":1787325073851,"local_bytes":5336,"local_packets":19,"other_bytes":5470,"other_packets":16,"total_bytes":10806,"total_packets":35},"interface":"br-lan","internal":true,"type":"flow_stats"} +``` + +| Field | Type | Notes | +|---|---|---| +| `digest` | string | Correlation key | +| `last_seen_at` | uint64 | Epoch milliseconds | +| `detection_packets` | uint | Packets DPI examined before finalizing classification (a detection-time count, not a running traffic count) | +| `local_bytes`, `other_bytes` | uint | **Per-update deltas, not cumulative.** See the correction below. | +| `local_packets`, `other_packets` | uint | Same: per-update deltas | +| `total_bytes`, `total_packets` | uint | **Cumulative** since flow start. This is the only running total in the record. | + +**Correction to a prior assumption, stated here as confirmed fact**: +`local_bytes`/`other_bytes` (and the matching packet counters) are +per-update deltas, not running totals; only `total_bytes`/`total_packets` +are cumulative. This was verified on hardware by observing directional +counters return to zero in the final `flow_purge` for a flow while +`total_bytes` did not (documented in this project's `DESIGN.md`; that +specific purge event was not part of the shorter capture excerpted in this +document). It also holds up as independent arithmetic in this capture's own +`flow_stats` records, without needing a purge event: for several digests, +`local_bytes + other_bytes` is an exact multiple of `total_bytes` +(consistent with equal-sized prior deltas already having accumulated into +the total), and never simply equal to it by coincidence across the whole +set. Two examples from the real data: + +| digest (first 8 hex) | local+other bytes (this update) | total_bytes | local+other pkts | total_packets | Reading | +|---|---|---|---|---|---| +| `7f3838be` | 240 | 480 | 2 | 4 | This update's delta is exactly half the cumulative total: one earlier, equal-sized update already happened. | +| `37be5b0b` | 189 | 378 | 3 | 6 | Same pattern: exactly 2x, one prior equal delta already folded into `total_bytes`. | +| `f663f9df` | 10806 | 10806 | 35 | 35 | Delta equals total exactly: consistent with this being the flow's first `flow_stats` update (nothing prior to sum in). | + +If `local_bytes`/`other_bytes` were themselves cumulative, the delta sum +would always equal the total, never a clean multiple of it. Records like +`7f3838be` and `37be5b0b` rule that out directly from this capture's data. + +### 3.6 `flow_purge` + +**Confirmed live**, across 10+ purge events captured separately during this +project's hardware testing (`DESIGN.md`), though none happened to fall +inside the specific capture session excerpted in this document (the two +newly detected flows were still active when that capture ended, and none of +the already-tracked flows from the initial `flow_stats` burst closed during +the window). The confirmed live behavior: `local_bytes`/`other_bytes` +(and the matching packet counters) are **always zero** on the closing purge +event; `total_bytes`/`total_packets` carry the flow's final cumulative +totals, unchanged from the flow's last known value going into the purge. +The example below is reconstructed for illustration, since this document's +own capture did not happen to include one, reusing a real digest from this +capture with counters set to that confirmed pattern: + +```json +{"established":false,"flow":{"digest":"9c2d7b56c4571e146f65b0d15c12cec661d56b3f","last_seen_at":1787325199999,"local_bytes":0,"other_bytes":0,"total_bytes":642,"local_packets":0,"other_packets":0,"total_packets":6,"reason":"closed"},"interface":"wan","internal":false,"type":"flow_purge"} +``` +**(Reconstructed for illustration: this document's own capture did not include a purge event, but the local_bytes/other_bytes=0, total_bytes=final pattern shown is confirmed live behavior, not speculation.)** + +| Field | Type | Notes | +|---|---|---| +| `digest` | string | Correlation key; the flow being removed | +| `last_seen_at` | uint64 | Epoch milliseconds | +| `local_bytes`, `other_bytes`, `local_packets`, `other_packets` | uint | **Always zero** on `flow_purge`, confirmed live across 10+ purge events (`DESIGN.md`). The flow's final directional split is not available on this event; see the accounting note below. | +| `total_bytes`, `total_packets` | uint | Final cumulative totals for the flow's whole lifetime | +| `reason` | string | One of `"closed"` (e.g. TCP FIN), `"expired"` (idle timeout), `"terminated"` (e.g. agent shutdown) | + +Emitted when a flow leaves the tracking table: normal close, idle expiry, or +agent shutdown. This is the last event you will see for a given digest. +Because the directional counters are zeroed on purge, a consumer cannot read +a flow's final up/down split off this event directly; this project's daemon +instead recovers it by distributing the `total_bytes` delta since the last +known total across up/down using the flow's established ratio from its +prior `flow_stats` updates. + +## 4. Consumer-relevant caveat on `agent_status` + +Repeated from section 3.3 for visibility: never rely on `agent_status` +carrying `agent_version`/`devices`/`interfaces`/`stats`. Only the base +process-telemetry fields are guaranteed on every frame. + +## 5. Naming and id spaces + +Two id spaces coexist in `detected_protocol`/`detected_application`, and +they are **not** globally unique against each other -- this is a genuine +footgun confirmed directly from the real `definitions` payload. + +- **nDPI-native protocol detections** (`detected_protocol` / `protocols[]` + in `definitions`): plain, unnamespaced strings -- `"DNS"`, `"HTTP"`, + `"HTTP/S"`, `"BitTorrent"`, `"WireGuard"`, `"Tailscale"`, `"ZOOM"`, etc. + 217 entries in this capture, ids observed from `0` (`"Unknown"`) up to + `316` (`"Meraki/Cloud"`), plus a sentinel id `4294967295` (`0xFFFFFFFF`, + uint32 max) tagged `"TODO"` whose meaning is not confirmed by any source + consulted (placeholder for an unassigned/pending signature, presumably, + but unverified). + +- **Netify application-layer detections** (`detected_application` / + `applications[]` in `definitions`): use the `netify.` naming + convention when a specific app/service was identified on top of the + transport protocol -- `netify.netflix`, `netify.tailscale`, + `netify.openwrt`, etc. 416 entries in this capture, 199 of which are + `netify.`-prefixed; the remaining 217 non-`netify.`-prefixed entries in + the *applications* array are the same low-numbered nDPI protocol tags + mirrored into that array as well (e.g. id `156` appears in `applications[]` + tagged `"Spotify"` *and* separately in `applications[]` tagged... no -- + more precisely: id `156` is `"Spotify"` in `protocols[]` and + `"netify.spotify"` in `applications[]`. These are two different arrays + with independently assigned ids that happen to collide numerically. + +**The id-collision gotcha, confirmed directly from the real captured +`definitions` payload:** + +| Numeric id | Meaning in `protocols[]` | Meaning in `applications[]` | +|---|---|---| +| 121 | `Dropbox` | `netify.dropbox` | +| 156 | `Spotify` | `netify.spotify` | +| 206 | `WireGuard` | `netify.cloudflare` | +| 207 | `SMPP` | `netify.microsoft-onedrive` | + +`id 206` is the sharpest example: it is `WireGuard` in the protocol table +and `netify.cloudflare` in the application table -- two completely unrelated +things sharing a number. **Always resolve `detected_protocol` only against +`definitions.protocols[]`, and `detected_application` only against +`definitions.applications[]`. Never cross-reference an id between the two +arrays.** + +A second, related nuance: a widely-repeated simplification (also present in +this project's own `DESIGN.md`, stated there as a general rule of thumb) is +that netify application ids are `>= 10000` and nDPI protocol ids are +`< 10000`. That is the dominant pattern for *recently added* app signatures, +but it is not a hard rule: this capture's `applications[]` array contains at +least 26 legacy `netify.` entries with ids well under 10000, including +some very common ones: + +``` +68 netify.msn +70 netify.yahoo +119 netify.facebook +120 netify.twitter +121 netify.dropbox +122 netify.gmail +124 netify.youtube +133 netify.netflix +156 netify.spotify +195 netify.twitch +199 netify.snapchat +201 netify.instagram +202 netify.microsoft +206 netify.cloudflare +``` + +The reliable discriminator for "is this an app-level detection, not just a +protocol-level one" is the literal `netify.` string prefix on +`detected_application_name`, not the numeric id range. Use the prefix test, +not an id threshold. + +Cross-checked against the actual `netify-apps.conf` examples cited in prior +source-reading of this project (`app:133:netify.netflix`, +`app:10119:netify.adobe`, `app:10552:netify.tesla`, `app:199:netify.snapchat`): +all four id-to-slug pairs match exactly what's in this capture's +`definitions.applications[]` array, which is good cross-validation that the +signature-file mapping and the live socket's id table are the same data. + +## 6. Category files on disk + +`/etc/netify.d/netify-categories.json` (installed by the `netifyd` package, +not vendored by this repo) provides the category taxonomy: + +- `application_tag_index` / `protocol_tag_index` -- numeric category id to + name, e.g. `streaming-media`, `social-media`, `games`, `voip`, `cdn`, + `advertiser`, `malware`, `os-software-updates`. +- `application_index` / `protocol_index` -- numeric category id to the + member application/protocol ids in that category. + +These map to `flow.category.application` / `flow.category.protocol` / +`flow.category.domain` (see section 3.4). No category object was captured in +enough detail in this session to re-derive the full tag list independently; +figures below are as previously documented and are called out where they +disagree. + +**Count discrepancy, unresolved**: prior source-reading of this project +enumerated exactly 31 application-category tags and 17 protocol-category +tags by name. This project's `DESIGN.md` states 32 and 18 respectively +(without enumerating the 32nd/18th). Neither the category JSON file itself +nor a `flow.category.*` value dense enough to reverse-engineer the missing +entry was available for this document. Treat `DESIGN.md`'s counts as +authoritative per this project's convention, but be aware the exact +32nd application tag and 18th protocol tag were not independently confirmed +here. + +**Licensing**: both the signature database that drives application/protocol +detection and the generated category file are proprietary data owned by +eGloo/Netify, shipped as part of the `netifyd` package itself, not part of +netifyd's GPLv3 source distribution in the way the daemon code is. This +project reads them at runtime from the paths above and vendors none of this +data in its own repository. (Prior source-level reading found the explicit +proprietary-license header specifically inside a signature pattern-matching +file distinct from the plain id-to-slug `netify-apps.conf`; that specific +filename-level detail was not independently re-confirmed for this document +and is listed under "Unverified / unknown." The operative conclusion, that +none of this data should be vendored or redistributed, is confirmed by both +sources independently and is not in question.) + +## 7. Practical consumer guidance + +**Join on digest.** `flow.digest` and `flow.last_seen_at` are the only two +fields serialized unconditionally across every event type that carries a +`flow` object. Key your flow table by `digest`. Do not assume any other +field is always present -- `flow` carries identity and no counters, +`flow_stats` carries counters and no identity, `flow_purge` carries a final +cumulative `total_bytes`/`total_packets` plus a reason, but zeroed +directional counters (§3.6): it does not carry a final directional split. + +**Stub-flow pattern.** Because `dump_established_flows` does not exist in +4.4.7 (section "Scope" above), and because this is directly demonstrated by +the real capture (11 `flow_stats` records for digests never announced via +`flow` on this connection), a consumer must tolerate `flow_stats` or +`flow_purge` arriving for an unknown digest. Do not drop these events. +Instead, create a placeholder entry (interface/internal known from the +envelope, identity fields set to an "Unknown" sentinel) and backfill +identity if and when a `flow` event for that same digest eventually arrives. +It may never arrive, if the flow was already fully classified before this +connection started and its classification does not get re-announced. + +**Delta clamping rationale.** Because `local_bytes`/`other_bytes` are +per-update deltas while `total_bytes` is the only cumulative field, a +consumer accounting a running rate should sum the directional deltas per +interval directly. But right after a reconnect (where a stub flow has no +prior history), or after any missed update, naively summing deltas risks +overshooting the flow's real observed growth. Clamp the accounted delta so +the consumer's own running total for that digest never exceeds +`total_bytes`'s growth: `accounted_delta = min(local_bytes + other_bytes, +total_bytes - last_known_total)`. This is the approach this project's daemon +takes, and it is the correct general-purpose defense against the blind-spot +created by the missing backfill option. + +**Resolve ids against the matching array only.** `detected_protocol` against +`definitions.protocols[]`; `detected_application` against +`definitions.applications[]`; `flow.category.*` against +`netify-categories.json`'s index tables. All three are separate id spaces +that can and do collide numerically (section 5). + +## 8. Unverified / unknown + +Collected here in one place rather than scattered as caveats only: + +- **`established` (top-level boolean)** -- precise semantics not confirmed + by source reading or by this capture (always `false` in the sample, which + is too uniform to infer meaning from). +- **`update_imf`** (in `agent_status`) -- present, value `1` in both captured + frames, meaning not confirmed by any source consulted. +- **`first_update_at`** (in `flow`) -- a real field, not previously + documented for this interface. Identical to `first_seen_at` in both + captured examples (both first-detection events); whether/how it diverges + when `detection_updated` fires is unconfirmed. +- **The `agent_status` timing anomaly** -- `uptime` advanced 15s between two + captured frames while `timestamp` advanced only 1s. Not explained; may be + a genuine daemon quirk or a capture-environment artifact. Flagged, not + asserted as behavior to depend on. +- **`risks.risks[]` numeric values** (e.g. `46` observed) -- no id-to-name + mapping found in any netifyd-side source or doc consulted. These are + presumably nDPI's own risk-flag enum (documented upstream in nDPI, not in + netifyd), which was out of scope to chase down for this document. +- **`other_type` full enum** -- only `"local"` was observed live. The other + values (`remote`, `broadcast`, `multicast`, `unsupported`, `error`, + `unknown`) come from source reading only, not from this capture. +- **`ct_id`/`ct_mark`/conntrack correlation** -- absent from both captured + `flow` examples despite `conntrack` being a compiled-in capability per + `build_version`. Unconfirmed whether that's because these are UDP/no-NAT + flows or because emission requires a config flag not on by default. +- **Category tag counts** -- 31/17 (prior source enumeration, with names) vs + 32/18 (`DESIGN.md`, without full enumeration). Not reconciled; see + section 6. +- **Whether `detected_application_name` can be absent/empty** for + protocol-only detections -- asserted by source reading, not observed + empty in this capture (both examples had an app name set). +- **The specific filename carrying the proprietary-license header** on the + signature data (`netify-apps.conf` vs. a separate pattern-matching file) -- + see the licensing note in section 6. +- **TCP socket default port** -- irrelevant to OpenWrt's default + UNIX-socket-only configuration, so omitted from the main body, but never + resolved in prior research: conflicting values (`7150` in man-page prose, + `2100` in a shipped sample config) were found and neither was checked + against a running instance with TCP enabled. +- **`http.*`, `dhcp.fingerprint`, `gtp.*` sub-objects** -- none observed in + this capture (no flows of the relevant type were present); documented from + source only. + +## 9. Sources + +- Direct hardware capture: Linksys EA8500, OpenWrt 25.12.5, netifyd 4.4.7, + UNIX socket capture immediately following daemon start (this document's + primary evidence for every "real captured example"). +- This project's `docs/DESIGN.md`, sections 2 and 2.4 (separately + hardware-verified facts, treated as authoritative wherever they conflict + with source-only reading). +- netifyd 4.4.7 source at the exact shipped tag: + `gitlab.com/netify.ai/public/netify-agent` (tag `v4.4.7`), in particular + `src/netifyd.cpp`, `src/nd-detection.cpp`, `src/nd-json.cpp`, + `src/nd-socket.cpp`, `include/nd-flow.h`, `include/netifyd.h`, + `deploy/netifyd.conf.in`, `deploy/netify-categories.json`, + `deploy/netify-apps.conf`, `doc/netifyd.conf.5.in`, + `doc/README-JSON-socket-example.md`, `doc/json-socket-filter.jq`. +- OpenWrt packaging: `github.com/openwrt/packages`, `net/netifyd/` + (Makefile, `files/netifyd.init`, `files/netifyd.config`). diff --git a/luci-app-appflow/docs/overview.png b/luci-app-appflow/docs/overview.png new file mode 100644 index 000000000..00f35d321 Binary files /dev/null and b/luci-app-appflow/docs/overview.png differ diff --git a/luci-app-appflow/docs/statistics.png b/luci-app-appflow/docs/statistics.png new file mode 100644 index 000000000..fcd6600a3 Binary files /dev/null and b/luci-app-appflow/docs/statistics.png differ diff --git a/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js b/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js new file mode 100644 index 000000000..96c01b700 --- /dev/null +++ b/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js @@ -0,0 +1,1463 @@ +'use strict'; +'require baseclass'; +'require request'; +'require rpc'; + +/* + * AppFlow: shared frontend helpers. + * + * Everything the two views have in common lives here: the ubus bindings from + * DESIGN.md §5, a thin normalisation layer over the daemon payloads, byte/rate + * formatting, the category colour scheme and the SVG chart primitives. + * + * The charts are hand-drawn SVG on purpose (see docs/frontend.md): SVG reads + * the theme's CSS custom properties directly, so identical markup stays legible + * on the light and dark bootstrap themes with no JS colour probing. + */ + +/* ------------------------------------------------------------------ ubus */ + +/* No `expect` filter: the daemon's exact envelope is still settling and an + * expect type mismatch would silently swallow a valid reply. `reject: true` + * makes a non-zero ubus status reject (4 = object not found, i.e. appflowd is + * down; 6 = permission denied, i.e. an ACL gap), which is what the views key + * their "not running" state off. */ +function declare(method, params) { + return rpc.declare({ + object: 'appflow', + method: method, + params: params, + reject: true + }); +} + +/* §5 also defines `apps` and `flows`. + * + * `apps` IS bound as of 2026-08-28, because the overview grew a device + * drill-down: clicking a device asks what that device is doing, and the answer + * is `apps {device}`. Bound here AND granted in the ACL, in that order, exactly + * as the note below required. + * + * That call returns an AGGREGATE per (device, application) computed from the + * live flow table. It is more revealing than either list alone, since it says + * which device is using which application, and it is still far short of + * `flows`: no remote address, no port, no per-connection record. `flows` + * remains unbound and ungranted. + * + * The original note, which still governs `flows`: A security audit pointed out that this comment was the + * evidence: the frontend had been deliberately narrowed and the boundary file + * had not, so an account restricted to this package alone could still call + * `flows` directly through /cgi-bin/luci/admin/ubus. That one matters more than + * the others, because every method the UI does use returns an AGGREGATE, while + * `flows` is the live per-connection table -- which local device is talking to + * which remote endpoint, right now. If a view grows to need either, bind it + * here AND add it back to the ACL, in that order. + * `params` is positional: callStats('hour'), callDetail('a10465'). */ +var callStatus = declare('status'), + callSummary = declare('summary'), + /* positional: devices(sort, limit, apps). `apps` restricts the answer to + * the devices carrying those application keys, which is how the overview + * answers "who is generating this traffic" once the user has filtered. */ + callDevices = declare('devices', [ 'sort', 'limit', 'apps' ]), + /* positional: apps(sort, limit, device). `device` scopes the answer to + * one device, which is the overview's drill-down. */ + callApps = declare('apps', [ 'sort', 'limit', 'device' ]), + callStats = declare('stats', [ 'range' ]), + callDetail = declare('app_detail', [ 'app' ]), + callReset = declare('reset'); + +/* ------------------------------------------------------------- constants */ + +var STYLE_ID = 'appflow-style'; + +/* + * Optional icon pack (DESIGN §9 soft coupling). The sibling package + * luci-app-appflow-icons installs a manifest plus SVGs under + * /luci-static/resources/appflow-icons/. Nothing here depends on it: the + * manifest is probed once per page load and, when it is absent, every tile + * stays a letter tile for the rest of the session. + * + * iconMap === null probe has not run yet + * iconMap === {} pack absent or unreadable, do not ask again + */ +var ICON_BASE = L.resource('appflow-icons'), + ICON_FILE = /^[A-Za-z0-9._-]+\.svg$/, + iconMap = null, + iconProbe = null; + +/* Mid-luminance hues: white tile text is legible on every entry, and each + * colour keeps enough contrast against both the white and the #222 page. */ +var PALETTE = [ + 'hsl(210,58%,46%)', 'hsl(150,50%,36%)', 'hsl(27,70%,46%)', 'hsl(280,40%,52%)', + 'hsl(340,52%,50%)', 'hsl(190,55%,38%)', 'hsl(96,40%,38%)', 'hsl(256,44%,56%)', + 'hsl(12,60%,50%)', 'hsl(172,45%,34%)', 'hsl(44,58%,40%)', 'hsl(318,36%,46%)', + 'hsl(224,42%,58%)', 'hsl(84,38%,32%)', 'hsl(0,52%,46%)', 'hsl(300,30%,42%)', + 'hsl(200,60%,32%)', 'hsl(62,42%,34%)' +]; + +/* + * Hashing a category name into PALETTE is fine for a tile, but the doughnut + * legend uses the same colours as a key, and with ~32 netify category tags in + * 18 buckets collisions are common enough to be seen (web and social-media + * collided on the first live run). The tags that actually show up get a fixed + * slot so the legend is unambiguous; anything outside this list still hashes. + */ +var CATEGORY_SLOT = { + 'web': 0, 'os-software-updates': 1, 'networking': 2, + 'infrastructure': 3, 'social-media': 4, 'messaging': 5, + 'cdn': 6, 'technology': 7, 'shopping': 8, + 'financial': 9, 'business': 10, 'games': 11, + 'file-sharing': 12, 'mail': 13, 'streaming-media': 14, + 'advertiser': 15, 'voip': 16, 'news': 17 +}; + +/* 'unclassified' is a real netify category; 'other' is our own roll-up of the + * slices past the top N. They are different things, so they get different + * greys rather than colliding in the doughnut legend. */ +var NEUTRAL = 'hsl(0,0%,50%)', + NEUTRAL_ROLLUP = 'hsl(0,0%,72%)'; + +/* + * EVERY category tag, not only the ones whose title-case reads badly. + * + * Until 1.1.1 this table held twelve entries, the ones where naive title-case + * produced the wrong English ('cdn' -> 'Cdn'). Everything else fell through to + * the title-caser at the end of catLabel(), which is string manipulation and + * not translation: 'web' became 'Web' by an algorithm, in English, in every + * language, for ever. `_()` never saw those strings, so the extractor never + * put them in the catalogue and no translator could reach them. Reported by + * @ntbowen against 1.1.0. + * + * The keys are the union of application_tag_index and protocol_tag_index from + * netifyd 4.4.7's /etc/netify.d/netify-categories.json, read off the bench, + * plus appflow's own four. A tag outside this list still title-cases and is + * still untranslated, which is a graceful degradation rather than a failure; + * frontend-suite.js holds its own copy of the list and goes red when one is + * missing, so a netifyd update that adds a tag is a visible event. + * + * KEEP THE ENGLISH EXACTLY AS THE TITLE-CASER PRODUCED IT where it was already + * right. These strings are shipped and screenshotted; this change is about how + * a label is produced, not what it says. + */ +var CATEGORY_LABELS = { + /* netifyd, application_tag_index */ + 'adult': _('Adult'), + 'advertiser': _('Advertiser'), + 'business': _('Business'), + 'cdn': _('CDN'), + 'cybersecurity': _('Cybersecurity'), + 'device-iot': _('IoT Devices'), + 'education': _('Education'), + 'entertainment': _('Entertainment'), + 'file-sharing': _('File Sharing'), + 'financial': _('Financial'), + 'gambling': _('Gambling'), + 'games': _('Games'), + 'government': _('Government'), + 'hosting': _('Hosting'), + 'mail': _('Mail'), + 'malware': _('Malware'), + 'messaging': _('Messaging'), + 'news': _('News'), + 'os-software-updates': _('OS & Software Updates'), + 'portal': _('Portal'), + 'recreation': _('Recreation'), + 'reference': _('Reference'), + 'remote-desktop': _('Remote Desktop'), + 'shopping': _('Shopping'), + 'social-media': _('Social Media'), + 'sports': _('Sports'), + 'streaming-media': _('Streaming Media'), + 'technology': _('Technology'), + 'telco': _('Telco'), + 'unclassified': _('Unclassified'), + 'voip': _('VoIP'), + 'vpn-and-proxy': _('VPN & Proxy'), + + /* netifyd, protocol_tag_index: the tags the application index does not + * also carry. A flow classified by protocol rather than by application + * reaches the browser through exactly the same field. */ + 'authentication': _('Authentication'), + 'database': _('Database'), + 'file-server': _('File Server'), + 'infrastructure': _('Infrastructure'), + 'media': _('Media'), + 'media-provider': _('Media Provider'), + 'networking': _('Networking'), + 'printing': _('Printing'), + 'proxy': _('Proxy'), + 'vpn': _('VPN'), + 'web': _('Web'), + + /* appflow's own, from the AI breakout. The daemon sends these as slugs + * for exactly this reason; see AI_HOSTS in appflowd. */ + 'ai-assistants': _('AI assistants'), + 'ai-developer': _('AI developer'), + 'ai-infrastructure': _('AI infrastructure'), + 'ai-media': _('AI media') +}; + +/* + * appflowd collapses unattributed traffic into three synthetic pseudo-devices + * and sends their display name already rendered in English, because a daemon + * has no idea what language a browser wants. It also sends the stable key + * behind that name, so the key is what we translate on: keying on the English + * would mean a daemon-side wording change silently reverting the translation. + * Every real device keeps the name the daemon resolved from its DHCP lease. + * Reported by @ntbowen against 1.1.0, and this is his fix. + */ +var DEVICE_LABELS = { + 'router': _('Router'), + 'unknown': _('Unknown'), + 'multicast': _('Multicast / Broadcast') +}; + +/* Field-name aliases. + * + * appflowd speaks two dialects: the live aggregates (summary / apps / devices) + * use bytes_down / bytes_up / bytes_total / rate_down / rate_up / rate_total, + * while the range views (stats / app_detail) use download / upload / total. + * Every read goes through these lists, so the views never care which one they + * are looking at and a future rename costs one edit here. + * + * Note that `tag` is deliberately NOT a category alias: on the live aggregates + * it carries the raw detection tag ("netify.openwrt"), not a category. */ +/* `dl` and `ul` are this module's OWN output names, listed here so the + * normalisers are idempotent. Without them normSeries() was destructive on a + * second pass: it reads components through these lists and the total through + * A_TOT, so re-normalising already-normalised data yielded + * { t: , dl: 0, ul: 0, total: } + * -- a total that contradicts its own parts. statistics.js:380 does exactly + * that when it falls through to `app.series`, which normApp() has already + * normalised, and columnChart reads only p.dl/p.ul, so it drew an EMPTY framed + * plot under a KPI row reporting megabytes. Worse, series.length was still 12, + * so the honest "No time series available" branch never ran. + * Found by a code-review panel, 2026-08-25. No daemon field is named dl or ul, + * so the aliases are inert against real payloads. */ +var A_DL = [ 'download', 'bytes_down', 'rx_bytes', 'download_bytes', 'total_download', 'dl' ], + A_UL = [ 'upload', 'bytes_up', 'tx_bytes', 'upload_bytes', 'total_upload', 'ul' ], + A_TOT = [ 'total', 'bytes_total', 'total_bytes', 'total_traffic' ], + A_RDL = [ 'rate_down', 'rx_rate', 'download_rate', 'down_rate' ], + A_RUL = [ 'rate_up', 'tx_rate', 'upload_rate', 'up_rate' ], + A_RATE = [ 'rate_total', 'rate', 'total_rate' ], + A_KEY = [ 'key', 'app', 'application', 'application_id', 'app_identifier', 'id' ], + A_LBL = [ 'label', 'name', 'application_name', 'app_name', 'app', 'application' ], + A_CAT = [ 'category', 'category_name', 'cat' ], + A_HOST = [ 'host', 'hostname', 'host_server_name', 'domain' ], + A_MAC = [ 'mac', 'local_mac', 'mac_address', 'macaddr' ], + A_IP = [ 'ip', 'local_ip', 'ipaddr', 'address' ], + A_DNAM = [ 'name', 'hostname', 'label', 'alias', 'device' ], + A_SEEN = [ 'last_seen', 'last_seen_at', 'last_active', 'last_active_time', 'seen' ], + A_TIME = [ 'ts', 't', 'time', 'timestamp', 'interval_start', 'start' ], + A_NFLOW = [ 'flows', 'active_flows', 'flow_count' ], + /* Raw netify detection tag: named 'tag' on the live aggregates and on + * app_detail, carried in 'app' on the range views. Icon lookups key off + * this, never off the display label. */ + A_TAG = [ 'tag', 'app' ]; + +/* ------------------------------------------------------------------- css */ + +var CSS = '\ +.af-grid{display:grid;grid-template-columns:1fr;gap:0 1em}\ +@media (min-width:920px){.af-grid.af-split{grid-template-columns:minmax(0,1.9fr) minmax(270px,1fr)}}\ +.af-card{border:1px solid var(--border-color-medium);border-radius:4px;\ + background:var(--background-color-high);padding:.8em 1em 1em;margin:0 0 1em}\ +.af-cardhead{display:flex;align-items:baseline;justify-content:space-between;\ + gap:.75em;flex-wrap:wrap;margin:0 0 .8em}\ +.af-cardhead h3{margin:0;padding:0;font-size:15px;font-weight:700;border:none}\ +.af-cardaux{display:flex;align-items:center;gap:.7em;flex-wrap:wrap;\ + font-size:11px;color:var(--text-color-medium)}\ +.af-sub{color:var(--text-color-medium);font-size:11px;line-height:14px;display:block}\ +.af-app .af-sub,.af-legend .af-legend-lbl,.af-applabel{overflow:hidden;\ + text-overflow:ellipsis;white-space:nowrap}\ +.af-empty .af-sub{font-size:12px;line-height:16px}\ +.af-mono{font-family:var(--font-mono);font-size:10.5px}\ +.af-num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums;\ + font-feature-settings:"tnum" 1}\ +.af-strip{display:flex;flex-wrap:wrap;gap:.4em;margin:0 0 1em}\ +.af-pill{display:inline-flex;align-items:center;gap:.45em;font-size:11px;\ + line-height:1;padding:.45em .75em;border-radius:11px;\ + border:1px solid var(--border-color-medium);color:var(--text-color-medium);\ + background:var(--background-color-low)}\ +.af-pill > i{width:7px;height:7px;border-radius:50%;background:var(--text-color-low)}\ +.af-pill.af-ok > i{background:var(--success-color-high,hsl(150,50%,36%))}\ +.af-pill.af-warn{color:var(--error-color-high);border-color:var(--error-color-high)}\ +.af-pill.af-warn > i{background:var(--error-color-high)}\ +.af-pill.af-unknown > i{background:var(--text-color-low);opacity:.55}\ +.af-tile{display:inline-flex;align-items:center;justify-content:center;\ + width:26px;height:26px;flex:0 0 26px;border-radius:5px;color:#fff;\ + font-weight:600;font-size:12px;text-shadow:0 1px 1px rgba(0,0,0,.25)}\ +.af-tile.af-tile-icon{font-size:0}\ +.af-tile-img{width:64%;height:64%;object-fit:contain;display:block}\ +.af-tile-img.af-mono{filter:brightness(0) invert(1)}\ +.af-app{display:flex;align-items:flex-start;gap:.6em;min-width:0}\ +.af-appname{min-width:0;overflow:hidden}\ +.af-applabel{display:block;overflow:hidden;text-overflow:ellipsis;\ + white-space:nowrap;line-height:16px}\ +.af-bar{position:relative;display:flex;height:8px;min-width:56px;border-radius:4px;\ + background:var(--background-color-medium);overflow:hidden;\ + box-shadow:inset 0 0 0 1px var(--border-color-low)}\ +.af-bar > i{display:block;height:100%;transition:width .3s ease-out}\ +.af-bar > i:last-child{border-top-right-radius:4px;border-bottom-right-radius:4px}\ +i.af-dl{background:var(--primary-color-high,hsl(210,58%,46%))}\ +i.af-ul{background:var(--success-color-high,hsl(150,50%,36%))}\ +.af-kpi{display:flex;flex-wrap:wrap;gap:1.4em 2.4em;margin:0 0 1.1em}\ +.af-kpi-lbl{font-size:10px;text-transform:uppercase;letter-spacing:.08em;\ + color:var(--text-color-medium);margin-bottom:.2em}\ +.af-kpi-val{font-size:21px;line-height:1.1;font-weight:600;\ + font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1}\ +.af-kpi-val.af-dlc{color:var(--primary-color-high,hsl(210,58%,46%))}\ +.af-kpi-val.af-ulc{color:var(--success-color-high,hsl(150,50%,36%))}\ +.af-plot{position:relative;padding-left:64px}\ +.af-plot > svg{display:block;width:100%;height:100%;overflow:visible}\ +.af-grid-line{stroke:var(--border-color-medium);stroke-width:1;opacity:.7}\ +.af-line{fill:none;stroke-width:2;stroke-linejoin:round;stroke-linecap:round}\ +.af-line.af-dl{stroke:var(--primary-color-high,hsl(210,58%,46%))}\ +.af-line.af-ul{stroke:var(--success-color-high,hsl(150,50%,36%))}\ +.af-area{stroke:none}\ +.af-area.af-dl{fill:var(--primary-color-high,hsl(210,58%,46%));fill-opacity:.16}\ +.af-area.af-ul{fill:var(--success-color-high,hsl(150,50%,36%));fill-opacity:.12}\ +rect.af-col.af-dl{fill:var(--primary-color-high,hsl(210,58%,46%))}\ +rect.af-col.af-ul{fill:var(--success-color-high,hsl(150,50%,36%))}\ +.af-yaxis{position:absolute;left:0;top:0;bottom:0;width:58px;pointer-events:none}\ +.af-ylbl{position:absolute;right:0;transform:translateY(-50%);font-size:10px;\ + color:var(--text-color-medium);font-variant-numeric:tabular-nums;white-space:nowrap}\ +.af-xaxis{display:flex;justify-content:space-between;padding-left:64px;\ + font-size:10px;color:var(--text-color-medium);margin-top:.8em}\ +.af-key{display:flex;gap:1.5em;flex-wrap:wrap;margin-top:.7em;font-size:11px}\ +.af-key > span{display:inline-flex;align-items:center;gap:.4em}\ +.af-key strong{font-size:12px}\ +.af-dot{display:inline-block;width:9px;height:9px;border-radius:2px;flex:0 0 9px}\ +.af-donut-wrap{display:flex;align-items:center;gap:1.2em;flex-wrap:wrap}\ +.af-pick{cursor:pointer;border-radius:3px}\n.af-pick:hover,.af-pick:focus{background:var(--background-color-medium);outline:none}\n.af-chipbar{margin:0 0 .5em}\n.af-chip{display:inline-flex;align-items:center;gap:.5em;padding:.15em .6em;border-radius:12px;background:var(--background-color-medium);font-size:12px}\n.af-chip-x{text-decoration:none;font-weight:bold;opacity:.7}\n.af-chip-x:hover{opacity:1}\ +.af-pick:hover,.af-pick:focus{background:var(--background-color-medium);outline:none}\ +.af-filter{display:flex;gap:.5em;align-items:center;margin:0 0 .6em}\ +.af-filter input{flex:1 1 auto;min-width:8em}\ +.af-filter-note{font-size:11px;opacity:.75}\ +.af-donut{position:relative;width:128px;height:128px;flex:0 0 128px}\ +.af-donut > svg{display:block;width:100%;height:100%}\ +.af-donut-track{stroke:var(--background-color-medium)}\ +.af-donut-mid{position:absolute;left:0;right:0;top:0;bottom:0;display:flex;\ + flex-direction:column;align-items:center;justify-content:center;\ + text-align:center;pointer-events:none}\ +.af-donut-mid strong{font-size:14px;font-variant-numeric:tabular-nums}\ +.af-donut-mid span{font-size:9px;color:var(--text-color-medium);\ + text-transform:uppercase;letter-spacing:.07em}\ +.af-legend{list-style:none;margin:0;padding:0;flex:1 1 145px;min-width:0}\ +.af-legend > li{display:flex;align-items:center;gap:.5em;padding:.22em 0;font-size:12px}\ +.af-legend-lbl{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;\ + white-space:nowrap}\ +.af-legend-val{flex:0 0 auto;color:var(--text-color-medium);font-size:11px}\ +.af-table{margin-bottom:0}\ +.af-table .th,.af-table .td{text-align:start;padding:7px 10px}\ +.af-table .tr.af-row{cursor:pointer}\ +.af-table .tr.af-row:hover .td{background:var(--background-color-low)}\ +.af-table .tr.af-all .td{background:var(--background-color-medium);font-weight:600}\ +.af-table .th.af-num,.af-table .td.af-num{text-align:right}\ +.af-table .th.af-sortable{cursor:pointer;user-select:none;white-space:nowrap}\ +.af-table .th.af-sortable:hover{color:var(--primary-color-high,hsl(210,58%,46%))}\ +.af-caret{font-size:9px;margin-left:.3em;opacity:.6}\ +.af-placeholder{padding:1.7em 0;text-align:center;color:var(--text-color-medium);\ + font-size:12px}\ +.af-empty{text-align:center;padding:2.2em 1em}\ +.af-empty svg{width:34px;height:34px;fill:var(--warn-color-high);margin-bottom:.3em}\ +.af-empty h3{margin:0 0 .5em;border:none;font-size:16px}\ +.af-empty p{max-width:52em;margin:0 auto .8em;color:var(--text-color-medium)}\ +.af-cmd{display:inline-block;text-align:left;margin:0 auto .9em;padding:.6em .9em;\ + border:1px solid var(--border-color-medium);border-radius:3px;\ + background:var(--background-color-low);font-family:var(--font-mono);font-size:12px}\ +.af-toolbar{display:flex;align-items:center;gap:.6em;flex-wrap:wrap;margin:0 0 .9em}\ +.af-toolbar input[type="text"]{width:235px;max-width:100%}\ +.af-spacer{flex:1 1 auto}\ +.af-ranges{margin-bottom:1.2em}\ +.af-ranges > li.af-disabled,.af-ranges > li.af-disabled > a{cursor:not-allowed}\ +.af-modal-head{display:flex;align-items:center;gap:.75em;margin:0 0 1em}\ +.af-modal-head .af-tile{width:34px;height:34px;flex-basis:34px;font-size:15px;\ + border-radius:6px}\ +.af-modal-head h4{margin:0;font-size:16px;line-height:1.2;text-align:start}\ +.af-modal-head > div{text-align:start}\ +.af-badge{display:inline-block;font-size:10px;letter-spacing:.03em;padding:.2em .55em;\ + border-radius:9px;color:#fff;text-shadow:0 1px 1px rgba(0,0,0,.25)}\ +.af-btnrow{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:.5em;margin-top:.8em}\ +.af-tag{display:inline-block;margin-left:.45em;padding:0 .4em;border-radius:3px;\ + font-size:9px;line-height:14px;text-transform:uppercase;letter-spacing:.05em;\ + color:var(--text-color-medium);border:1px solid var(--border-color-high);\ + vertical-align:1px}\ +.modal h4{margin:1.1em 0 .5em;font-size:13px;text-transform:uppercase;\ + letter-spacing:.06em;color:var(--text-color-medium);border:none;text-align:start}\ +.modal .af-modal-head h4{margin:0;font-size:16px;text-transform:none;letter-spacing:0;\ + color:var(--text-color-high);text-align:start}\ +'; + +/* --------------------------------------------------------------- exports */ + +return baseclass.extend({ + rpc: { + status: callStatus, + summary: callSummary, + devices: callDevices, + apps: callApps, + stats: callStats, + detail: callDetail, + reset: callReset + }, + + /* ------------------------------------------------------ value picking */ + + num: function(obj, names, dflt) { + if (obj != null) + for (var i = 0; i < names.length; i++) { + var v = obj[names[i]]; + + if (v != null && v !== '' && typeof v != 'object' && !isNaN(+v)) + return +v; + } + + return (dflt != null) ? dflt : 0; + }, + + str: function(obj, names, dflt) { + if (obj != null) + for (var i = 0; i < names.length; i++) { + var v = obj[names[i]]; + + if (v != null && v !== '' && typeof v != 'object') + return String(v); + } + + return (dflt != null) ? dflt : ''; + }, + + /* A ubus payload may hand back either an array or a map keyed by id; + * accept both and always yield an array. */ + toList: function(value, keyField) { + if (Array.isArray(value)) + return value.filter(function(v) { return v != null; }); + + if (!value || typeof value != 'object') + return []; + + return Object.keys(value).map(function(k) { + var row = (value[k] && typeof value[k] == 'object') ? value[k] : {}, + out = {}; + + for (var f in row) + out[f] = row[f]; + + if (keyField && out[keyField] == null) + out[keyField] = k; + + return out; + }); + }, + + /* -------------------------------------------------------- normalisers */ + + /* Byte and rate counters, wherever they sit in the payload. */ + normTotals: function(raw) { + var dl = this.num(raw, A_DL), + ul = this.num(raw, A_UL), + tot = this.num(raw, A_TOT, -1), + rdl = this.num(raw, A_RDL), + rul = this.num(raw, A_RUL), + rate = this.num(raw, A_RATE, -1); + + return { + dl: dl, + ul: ul, + total: (tot >= 0) ? tot : dl + ul, + rdl: rdl, + rul: rul, + rate: (rate >= 0) ? rate : rdl + rul, + flows: this.num(raw, A_NFLOW) + }; + }, + + /* A dedicated rate object may name its members download/upload rather than + * rx_rate/tx_rate; fold both spellings into rdl/rul. */ + /* A `rates` envelope may name its fields either way: rate_down/rx_rate, or + * the byte-ish download/bytes_down, which inside a rates object still mean + * a RATE. So a fallback between the two lists is intended. + * + * What is not intended is `t.rdl || t.dl`, which is what this used to be. + * `||` cannot tell "the rate field is absent" from "the rate is legitimately + * zero", so an idle link carrying rate_down: 0 alongside a cumulative + * bytes_down fell through to the byte counter and the Download KPI read + * something like "12.4 GB/s" while nothing moved. Read with a -1 sentinel + * and test presence instead, the way normTotals already does for its own + * totals. Raised by a code-review panel, 2026-08-25. + * + * Latent rather than live: the only caller is guarded by `summary.rates ?`, + * and this daemon puts byte and rate counters together in `totals` and sends + * no `rates` object at all. This is the forward-looking path the comment in + * overview.js promises, so it should be correct before it is ever used. */ + normRates: function(raw) { + var rdl = this.num(raw, A_RDL, -1), + rul = this.num(raw, A_RUL, -1), + rate = this.num(raw, A_RATE, -1), + t = this.normTotals(raw); + + return { + rdl: (rdl >= 0) ? rdl : t.dl, + rul: (rul >= 0) ? rul : t.ul, + rate: (rate >= 0) ? rate : t.total + }; + }, + + normApp: function(raw) { + var t = this.normTotals(raw), + label = this.appLabel(this.str(raw, A_LBL, _('Unknown'))); + + t.key = this.str(raw, A_KEY, label); + t.label = label; + t.cat = this.str(raw, A_CAT); + t.host = this.str(raw, A_HOST); + t.tag = this.str(raw, A_TAG); + t.series = this.normSeries(raw ? (raw.time_series || raw.series) : null); + + return t; + }, + + normDevice: function(raw) { + var t = this.normTotals(raw), + key = this.str(raw, A_KEY), + mac = this.str(raw, A_MAC), + ip = this.str(raw, A_IP); + + /* The daemon's own device key, carried through because the overview + * drills into a device by it. It was dropped here, so dev.key was + * undefined at the call site and the drill-down silently did nothing: + * no error, no effect, a feature that looked wired and was not. + * + * Falls back to mac/ip so a row always has something to identify it, + * but the daemon's key is preferred: it is what the pseudo-devices + * (router, multicast, unknown) are addressed by, and those have no + * MAC at all. */ + t.key = this.str(raw, [ 'key', 'device', 'id' ], mac || ip || ''); + t.mac = mac.toUpperCase(); + t.ip = ip; + + /* The three synthetic pseudo-devices are named on the KEY, not on the + * name the daemon sent: that name is English and pre-rendered, and + * matching it as a string would mean any daemon-side rewording + * silently dropped back to English. Every real device keeps the name + * resolved from its DHCP lease. + * + * REUSE t.key, DO NOT RE-DERIVE IT. The contributor's patch added its + * own `key = this.str(raw, A_KEY)` here, and A_KEY is the APPLICATION + * alias list: it reaches for 'app' and 'application' before 'id', so a + * device row carrying either field would have been named from an + * application identifier. Two key derivations in one function that + * disagree is the bug; the device key is already sitting in t.key, + * three lines up, derived from the device alias list. + * + * hasOwnProperty for the reason given in catLabel(): a bare lookup on + * '__proto__' or 'constructor' returns a truthy object, and t.name is + * assumed to be a string by the .toUpperCase() on the next line. */ + t.name = Object.prototype.hasOwnProperty.call(DEVICE_LABELS, t.key) + ? DEVICE_LABELS[t.key] + : this.str(raw, A_DNAM, ip || mac || _('Unknown device')); + t.seen = this.num(raw, A_SEEN, 0); + t.isRouter = !!(raw && raw.is_router); + + /* the daemon falls back to the MAC when no lease name is known; do not + * then print the same string twice in the cell */ + t.sub = (t.name.toUpperCase() == t.mac) ? t.ip : (t.mac || t.ip); + + return t; + }, + + /* Secondary line under an application name: the server hostname when the + * payload carries one, otherwise the category, otherwise the flow count. */ + subLabel: function(app) { + if (app.host) + return app.host; + + if (app.cat) + return this.catLabel(app.cat); + + if (app.flows) + return this.fmtFlows(app.flows); + + return ''; + }, + + fmtFlows: function(count) { + var n = Math.max(0, Math.round(+count || 0)); + + return (n == 1) ? _('1 flow') : _('%d flows').format(n); + }, + + normSeries: function(raw) { + var self = this; + + return this.toList(raw).map(function(p) { + var dl = self.num(p, A_DL), + ul = self.num(p, A_UL), + tot = self.num(p, A_TOT, -1); + + return { + t: self.num(p, A_TIME, 0), + dl: dl, + ul: ul, + total: (tot >= 0) ? tot : dl + ul + }; + }); + }, + + /* --------------------------------------------------------- formatting */ + + /* LuCI's %m specifier only engages above the divisor and prints the raw + * float below it, so anything under 1 KiB is rounded here instead. */ + fmtBytes: function(bytes) { + var n = +bytes; + + if (!isFinite(n) || n < 0) + n = 0; + + return (n > 1024) ? '%1024.2mB'.format(n) : '%d B'.format(Math.round(n)); + }, + + fmtRate: function(bps) { + var n = +bps; + + if (!isFinite(n) || n < 0) + n = 0; + + return (n > 1024) ? '%1024.2mB/s'.format(n) : '%d B/s'.format(Math.round(n)); + }, + + fmtPercent: function(part, whole) { + if (!whole) + return '0.0%'; + + /* Clamp at 100: the app total and the per-device totals come from rings + * of different bucket granularity over the same trailing hour, so at the + * window edge a single device's total can round just above the app total. + * A share over 100% reads as a bug even when the few-byte cause is benign. */ + var pct = 100 * part / whole; + + return '%.1f%%'.format(pct > 100 ? 100 : pct); + }, + + fmtAgo: function(stamp) { + var ts = +stamp; + + if (!isFinite(ts) || ts <= 0) + return '-'; + + /* accept both second and millisecond epochs */ + if (ts < 1e11) + ts *= 1000; + + var d = Math.round((Date.now() - ts) / 1000); + + if (d < 0) + d = 0; + + if (d < 15) + return _('just now'); + + if (d < 60) + return _('%d s ago').format(d); + + if (d < 3600) + return _('%d min ago').format(Math.floor(d / 60)); + + if (d < 86400) + return _('%d h ago').format(Math.floor(d / 3600)); + + return _('%d d ago').format(Math.floor(d / 86400)); + }, + + fmtUptime: function(seconds) { + return '%t'.format(Math.max(0, Math.floor(+seconds || 0))); + }, + + /* "netify.google-ads" -> "Google Ads"; already-pretty names pass through. */ + appLabel: function(name) { + var s = String(name || '').replace(/^netify\./, ''); + + if (!s.length) + return _('Unknown'); + + if (/^[a-z0-9]+([-_.][a-z0-9]+)*$/.test(s)) + s = s.split(/[-_.]/).map(function(w) { + return w.charAt(0).toUpperCase() + w.substr(1); + }).join(' '); + + return s; + }, + + catLabel: function(cat) { + var raw = String(cat || ''); + + if (!raw.length) + return _('Unclassified'); + + /* THE TABLE IS CONSULTED FIRST, ahead of the pass-through below. + * + * Until 1.1.1 the pass-through came first, so nothing after it could + * ever be reached for a value carrying a capital. That is precisely + * how appflow's own four AI categories became permanently English in + * every language: the daemon sent "AI assistants", the pass-through + * returned it verbatim, and the lookup that would have translated it + * was three lines too late to run. The daemon now sends slugs, and + * this ordering means a future daemon-side display string can still be + * translated by adding one key rather than by changing the wire. + * + * hasOwnProperty, not a truthiness test: a plain object literal + * inherits Object.prototype, so CATEGORY_LABELS['__proto__'] and + * ['constructor'] are truthy OBJECTS and would be returned as if they + * were labels. color() below already guards its table this way. */ + var c = raw.toLowerCase(); + + if (Object.prototype.hasOwnProperty.call(CATEGORY_LABELS, c)) + return CATEGORY_LABELS[c]; + + /* Already-cased categories pass through untouched, exactly as + * appLabel() treats already-pretty application names. + * + * netifyd's own categories are lowercase slugs ("web", "networking"), + * so lowercasing them first is harmless. A capitalised category that + * is NOT in the table must survive as written: the title-case split is + * on [-_] and a space is neither, so "AI assistants" would otherwise + * come back as "Ai assistants". */ + if (/[A-Z]/.test(raw)) + return raw; + + return c.split(/[-_]/).map(function(w) { + return w.charAt(0).toUpperCase() + w.substr(1); + }).join(' '); + }, + + /* ------------------------------------------------------------ colours */ + + /* Stable hash of the category, so every app in a category shares its tile + * colour and the doughnut legend reads as one colour system. */ + color: function(key) { + var s = String(key == null ? '' : key).toLowerCase(); + + if (s == 'other') + return NEUTRAL_ROLLUP; + + if (!s.length || s == 'unclassified' || s == 'unknown') + return NEUTRAL; + + /* hasOwnProperty, not a bare lookup. `s` is attacker-influenced -- it + * comes from a category tag, an app label, or a DHCP hostname -- and a + * plain object literal inherits Object.prototype, so CATEGORY_SLOT + * ['constructor'], ['__proto__'] and ['toString'] all pass an + * `!= null` test and index PALETTE with a function, yielding undefined. + * A device named `constructor` then rendered style="background:undefined", + * which the browser drops, leaving white tile text on a transparent + * tile; in the doughnut the slice silently did not draw while its legend + * still claimed a percentage. Found by a code-review panel, 2026-08-25. */ + if (Object.prototype.hasOwnProperty.call(CATEGORY_SLOT, s)) + return PALETTE[CATEGORY_SLOT[s]]; + + var h = 0; + + for (var i = 0; i < s.length; i++) + h = ((h << 5) - h + s.charCodeAt(i)) | 0; + + return PALETTE[Math.abs(h) % PALETTE.length]; + }, + + /* --------------------------------------------------------- primitives */ + + /* + * Probe the optional icon pack. Resolves either way and never rejects, so + * callers can fold it into their load() without a catch: a missing pack is + * the normal case, not an error. + */ + loadIcons: function() { + if (iconProbe != null) + return iconProbe; + + iconProbe = request.get(ICON_BASE + '/manifest.json', { cache: true }) + .then(function(res) { + var map = res.ok ? res.json() : null; + + iconMap = (map != null && typeof map == 'object') ? map : {}; + }) + .catch(function() { + iconMap = {}; + }); + + return iconProbe; + }, + + /* + * Resolve a netify tag to an icon. simple-icons ship as fill-less glyphs + * that would render black on a coloured tile, so they are flagged for the + * white-out filter; full-colour logos are left alone. + */ + iconFor: function(tag) { + var t = String(tag || ''); + + if (iconMap == null || !t.length) + return null; + + /* Same prototype hazard as color() above: `t` is a daemon-supplied tag. */ + var e = Object.prototype.hasOwnProperty.call(iconMap, t) ? iconMap[t] : null; + + if (e == null || typeof e.file != 'string' || !ICON_FILE.test(e.file)) + return null; + + return { + url: ICON_BASE + '/svg/' + e.file, + mono: (e.source == 'simple-icons') + }; + }, + + tile: function(label, category, tag) { + /* STRIP ON A UNICODE CLASS, NOT ON ASCII. This used to be + * /^[^0-9A-Za-z]+/, which eats every leading character that is not an + * ASCII letter or digit. A name written in any non-Latin script is + * entirely non-ASCII, so the strip consumed the whole string, `s` came + * out empty and every such device and application rendered a '?' tile. + * + * That was not a translation bug: it hit any LAN client whose DHCP + * hostname was Chinese, Japanese, Korean, Russian, Greek, Arabic or + * Hebrew, on every release. Accepting the zh_Hans catalogue would have + * made it universal rather than occasional, which is how it was found. + * + * \p{L} and \p{N} need the /u flag. toUpperCase() is a no-op on + * scripts without case, which is correct rather than merely harmless. */ + var s = String(label || '').replace(/^[^\p{L}\p{N}]+/u, ''), + ch = s.length ? s.charAt(0).toUpperCase() : '?', + icon = this.iconFor(tag), + node = E('span', { + 'class': 'af-tile', + 'style': 'background:' + this.color(category || label), + 'aria-hidden': 'true' + }, [ ch ]); + + if (icon == null) + return node; + + /* The letter stays in the DOM behind font-size:0 and is only hidden once + * the icon has actually decoded, so a cold cache shows a letter that + * swaps to the logo rather than an empty coloured square, and a manifest + * entry pointing at a file that is not installed keeps the letter. */ + var img = E('img', { + 'class': 'af-tile-img' + (icon.mono ? ' af-mono' : ''), + 'src': icon.url, + 'alt': '', + 'load': function(ev) { + if (ev.target.parentNode) + ev.target.parentNode.classList.add('af-tile-icon'); + }, + 'error': function(ev) { + var host = ev.target.parentNode; + + if (host) { + host.removeChild(ev.target); + host.classList.remove('af-tile-icon'); + } + } + }); + + node.appendChild(img); + + /* already in cache: hide the letter now, before the first paint */ + if (img.complete && img.naturalWidth > 0) + node.classList.add('af-tile-icon'); + + return node; + }, + + /* `onPick` makes the application name and its category sub-label clickable, + * each filtering to itself. Everything visible that names a thing should be + * a way to see only that thing; a label that looks like a link and is not + * is worse than plain text. */ + appCell: function(app, sub, onPick) { + var second = (sub != null) ? sub : this.subLabel(app), + pick = (typeof onPick === 'function') ? onPick : null; + + var mk = function(cls, text, value) { + var a = { 'class': cls, 'title': text }; + + if (pick && value) { + a['class'] += ' af-pick'; + a.title = _('Show only %s').format(value); + a.click = function(ev) { ev.preventDefault(); pick(value); }; + } + + return E('span', a, [ text ]); + }; + + /* Colour seed: the category when the payload carries one, so every app + * in a category shares a tile colour; the stable app key otherwise. */ + return E('div', { 'class': 'af-app' }, [ + this.tile(app.label, app.cat || app.key || app.label, app.tag), + E('div', { 'class': 'af-appname' }, [ + mk('af-applabel', app.label, app.label), + second ? mk('af-sub', second, this.catLabel(app.cat) === second ? second : null) : '' + ]) + ]); + }, + + deviceCell: function(dev, onPick) { + var pick = (typeof onPick === 'function') ? onPick : null, + attrs = { 'class': 'af-app' }; + + if (pick) { + attrs['class'] += ' af-pick'; + attrs.title = _('Show what %s is doing').format(dev.name); + attrs.click = function(ev) { ev.preventDefault(); pick(dev); }; + } + + return E('div', attrs, [ + this.tile(dev.name, dev.mac || dev.name), + E('div', { 'class': 'af-appname' }, [ + E('span', { 'class': 'af-applabel', 'title': dev.name }, [ + dev.name, + dev.isRouter ? E('span', { 'class': 'af-tag' }, [ _('router') ]) : '' + ]), + dev.sub ? E('span', { 'class': 'af-sub af-mono' }, [ dev.sub ]) : '' + ]) + ]); + }, + + /* Two-tone download/upload bar, scaled against `max`. */ + /* `fmt` names the unit the caller is passing, because two of the three + * callers pass cumulative bytes and one passes rates. The tooltip formatted + * with fmtBytes unconditionally until 2026-08-25, so on the Overview's Top + * applications table the numeric cells read "1.20 MB/s" while hovering the + * bar in the same row read "Download: 1.20 MB" -- the same quantity stated + * twice, in two units, one of them wrong, and the wrong one being the one + * that looks cumulative. Fixed here rather than at the call site so the next + * caller cannot step on it, and so the translated string stops having to + * serve two unit systems at once. */ + bar: function(dl, ul, max, fmt) { + var m = (max > 0) ? max : 1, + w1 = Math.max(0, Math.min(100, 100 * dl / m)), + w2 = Math.max(0, Math.min(100 - w1, 100 * ul / m)), + f = (typeof fmt === 'function') ? fmt : this.fmtBytes; + + return E('div', { + 'class': 'af-bar', + 'title': _('Download: %s, upload: %s').format(f.call(this, dl), f.call(this, ul)) + }, [ + E('i', { 'class': 'af-dl', 'style': 'width:%.2f%%'.format(w1) }), + E('i', { 'class': 'af-ul', 'style': 'width:%.2f%%'.format(w2) }) + ]); + }, + + kpi: function(label, value, cls) { + return E('div', {}, [ + E('div', { 'class': 'af-kpi-lbl' }, [ label ]), + E('div', { 'class': 'af-kpi-val' + (cls ? ' ' + cls : '') }, [ value ]) + ]); + }, + + card: function(title, aux, body, cls) { + return E('div', { 'class': 'cbi-section af-card' + (cls ? ' ' + cls : '') }, [ + E('div', { 'class': 'af-cardhead' }, [ + E('h3', {}, [ title ]), + aux ? E('div', { 'class': 'af-cardaux' }, aux) : '' + ]), + body + ]); + }, + + placeholder: function(text) { + return E('div', { 'class': 'af-placeholder' }, [ text ]); + }, + + /* ------------------------------------------------------------- charts */ + + /* Round a peak up to a 1/2/5 * 10^n boundary so axis labels stay tidy. */ + niceMax: function(value) { + var v = +value; + + if (!isFinite(v) || v <= 0) + return 1; + + var exp = Math.floor(Math.log(v) / Math.LN10), + base = Math.pow(10, exp), + frac = v / base, + step = (frac <= 1) ? 1 : (frac <= 2) ? 2 : (frac <= 5) ? 5 : 10; + + return step * base; + }, + + svg: function(tag, attrs, children) { + var node = document.createElementNS('http://www.w3.org/2000/svg', tag); + + for (var k in (attrs || {})) + if (attrs[k] != null) + node.setAttribute(k, attrs[k]); + + (Array.isArray(children) ? children : (children != null ? [ children ] : [])) + .forEach(function(c) { + node.appendChild((typeof c == 'string') + ? document.createTextNode(c) : c); + }); + + return node; + }, + + yAxis: function(peak, fmt) { + return E('div', { 'class': 'af-yaxis' }, [ 1, 0.5, 0 ].map(function(f) { + return E('span', { + 'class': 'af-ylbl', + 'style': 'top:%.1f%%'.format(100 * (1 - f)) + }, [ (f > 0) ? fmt(peak * f) : '0' ]); + })); + }, + + xAxis: function(labels) { + if (!labels || !labels.length) + return ''; + + return E('div', { 'class': 'af-xaxis' }, [ + E('span', {}, [ labels[0] ]), + E('span', {}, [ labels[1] || '' ]) + ]); + }, + + gridLines: function(w, h) { + var out = []; + + for (var g = 0; g <= 4; g++) + out.push(this.svg('line', { + 'class': 'af-grid-line', + 'x1': 0, 'x2': w, + 'y1': (h * g / 4).toFixed(1), 'y2': (h * g / 4).toFixed(1), + 'vector-effect': 'non-scaling-stroke' + })); + + return out; + }, + + /* + * Rolling line chart. + * series: [ { values: [...], cls: 'af-dl'|'af-ul', fill: bool } ] + * opts: { slots, height, fmt, xlabels: [ left, right ] } + * + * Drawn in a 1000x300 user space stretched to the container width. Strokes + * carry vector-effect="non-scaling-stroke" so they stay 2px whatever the + * horizontal scale, and every label is real HTML so it never distorts. + */ + lineChart: function(series, opts) { + opts = opts || {}; + + var self = this, + W = 1000, H = 300, + slots = Math.max(2, opts.slots || 60), + fmt = opts.fmt || function(v) { return self.fmtRate(v); }, + peak = 0; + + series.forEach(function(s) { + (s.values || []).forEach(function(v) { + if (v > peak) + peak = v; + }); + }); + + peak = this.niceMax(peak * 1.05); + + var kids = this.gridLines(W, H); + + series.forEach(function(s) { + var vals = s.values || [], + n = vals.length; + + if (n < 1) + return; + + var pts = vals.map(function(v, i) { + var x = W * (slots - n + i) / (slots - 1), + y = H - H * Math.max(0, Math.min(1, v / peak)); + + return x.toFixed(2) + ',' + y.toFixed(2); + }); + + if (s.fill && n > 1) + kids.push(self.svg('path', { + 'class': 'af-area ' + (s.cls || ''), + 'd': 'M' + pts[0].split(',')[0] + ',' + H + + ' L' + pts.join(' L') + + ' L' + W + ',' + H + ' Z' + })); + + kids.push(self.svg('path', { + 'class': 'af-line ' + (s.cls || ''), + 'vector-effect': 'non-scaling-stroke', + 'd': 'M' + pts.join(' L') + })); + }); + + return E('div', { 'class': 'af-chart' }, [ + E('div', { + 'class': 'af-plot', + 'style': 'height:%dpx'.format(opts.height || 168) + }, [ + /* role=img with no accessible name announces as an unlabelled + * image, which is worse than leaving the role off entirely. + * Callers may pass opts.label; there is always a fallback. */ + this.svg('svg', { + 'viewBox': '0 0 %d %d'.format(W, H), + 'preserveAspectRatio': 'none', + 'role': 'img', + 'aria-label': opts.label || _('Traffic over time') + }, kids), + this.yAxis(peak, fmt) + ]), + this.xAxis(opts.xlabels) + ]); + }, + + /* + * Stacked download/upload column chart for a short bucket series + * (the 12 x 5-minute points of the "past hour" range). + */ + columnChart: function(points, opts) { + opts = opts || {}; + + /* `slots` is the width of the axis the CALLER labels, in buckets. A + * partial series is drawn right-aligned against it, so three buckets + * occupy the last three of sixty rather than being stretched across the + * whole width beneath a label reading "-60 min". + * + * That stretch was the behaviour until 2026-08-25, and it was not an + * edge case: this daemon keeps nothing across a restart by design, so + * every boot, every `uci commit appflow`, and every fresh install spent + * its first hour showing a chart that asserted an hour of history it did + * not have. lineChart already did this correctly for the same problem, + * which is what made it a defect rather than a decision. Omit `slots` + * and the old full-width behaviour is kept, for any caller that really + * is showing a complete series. */ + var self = this, + W = 1000, H = 240, + n = Math.max(1, points.length), + slots = Math.max(n, opts.slots || 0), + peak = 0; + + points.forEach(function(p) { + if (p.dl + p.ul > peak) + peak = p.dl + p.ul; + }); + + peak = this.niceMax(peak * 1.05); + + var kids = this.gridLines(W, H), + slot = W / slots, + bw = slot * 0.6, + pad = (slot - bw) / 2, + off = (slots - n) * slot; /* right-align a partial series */ + + points.forEach(function(p, i) { + var hdl = H * Math.max(0, Math.min(1, p.dl / peak)), + hul = H * Math.max(0, Math.min(1, p.ul / peak)), + x = off + i * slot + pad, + when = p.t + ? new Date(p.t < 1e11 ? p.t * 1000 : p.t) + .toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : '', + tip = '%s\n%s'.format(when, + _('Download: %s, upload: %s') + .format(self.fmtBytes(p.dl), self.fmtBytes(p.ul))); + + if (hdl > 0.4) + kids.push(self.svg('rect', { + 'class': 'af-col af-dl', + 'x': x.toFixed(2), 'y': (H - hdl).toFixed(2), + 'width': bw.toFixed(2), 'height': hdl.toFixed(2) + }, [ self.svg('title', {}, [ tip ]) ])); + + if (hul > 0.4) + kids.push(self.svg('rect', { + 'class': 'af-col af-ul', + 'x': x.toFixed(2), 'y': (H - hdl - hul).toFixed(2), + 'width': bw.toFixed(2), 'height': hul.toFixed(2) + }, [ self.svg('title', {}, [ tip ]) ])); + }); + + return E('div', { 'class': 'af-chart' }, [ + E('div', { + 'class': 'af-plot', + 'style': 'height:%dpx'.format(opts.height || 150) + }, [ + this.svg('svg', { + 'viewBox': '0 0 %d %d'.format(W, H), + 'preserveAspectRatio': 'none', + 'role': 'img', + 'aria-label': opts.label || _('Traffic per interval') + }, kids), + this.yAxis(peak, function(v) { return self.fmtBytes(v); }) + ]), + this.xAxis(opts.xlabels) + ]); + }, + + /* + * Doughnut. Pure SVG using stroke-dasharray on an r=15.91549 circle + * (circumference 100), so each slice length is literally its percentage. + */ + /* ------------------------------------------------------------ filter */ + + /* Parse a filter string into terms. + * + * Space separated, and EVERY term must pass, so terms narrow rather than + * widen. A leading minus excludes: + * + * ai only rows matching "ai" + * -netflix everything except Netflix + * ai -claude AI rows that are not Claude + * + * Exclusion exists because the question that prompted this was "one + * streaming session is burying everything else", and an include-only + * filter cannot express it: you would have to know in advance what you + * wanted to keep. + * + * A bare "-" is dropped rather than treated as excluding everything, which + * is what a half-typed term looks like. + */ + parseFilter: function(text) { + return String(text == null ? '' : text).toLowerCase() + .split(/\s+/) + .filter(function(t) { return t.length > 0; }) + .map(function(t) { + return (t.charAt(0) === '-') + ? { neg: true, term: t.slice(1) } + : { neg: false, term: t }; + }) + .filter(function(t) { return t.term.length > 0; }); + }, + + /* Does a row pass? `fields` are the strings the user could reasonably be + * typing at: an application label, its key, its category. + * + * An empty term list passes everything, which is the unfiltered case and + * must not be special-cased anywhere else. */ + matchFilter: function(terms, fields) { + var hay = (fields || []) + .filter(function(f) { return f != null && f !== ''; }) + .join(' ').toLowerCase(); + + return (terms || []).every(function(t) { + var hit = hay.indexOf(t.term) >= 0; + + return t.neg ? !hit : hit; + }); + }, + + donut: function(slices, opts) { + opts = opts || {}; + + var self = this, + total = slices.reduce(function(a, s) { return a + Math.max(0, s.value); }, 0), + /* An SVG circle's stroke starts at 3 o'clock, so a dashoffset of a + * quarter of the circumference (r is 15.91549, so C is ~100) starts + * the first slice at 12 o'clock. This is the ONLY rotation now: + * `.af-donut > svg` also carried transform:rotate(-90deg) until + * 2026-08-25, the two stacked, and the first slice actually began at + * 9 o'clock -- so this comment asserted something the render did not + * do. Found by a code-review panel. If you re-add a CSS rotation, + * take this offset out. */ + off = 25, + ring = [ this.svg('circle', { + 'class': 'af-donut-track', + 'cx': 21, 'cy': 21, 'r': 15.91549, + 'fill': 'none', 'stroke-width': 5.2 + }) ]; + + if (total > 0) + slices.forEach(function(s) { + var pct = 100 * Math.max(0, s.value) / total; + + if (pct <= 0) + return; + + ring.push(self.svg('circle', { + 'cx': 21, 'cy': 21, 'r': 15.91549, + 'fill': 'none', + 'stroke': s.color, + 'stroke-width': 5.2, + 'stroke-dasharray': '%.3f %.3f'.format(pct, 100 - pct), + 'stroke-dashoffset': off.toFixed(3) + }, [ self.svg('title', {}, [ '%s: %s (%s)'.format(s.label, + self.fmtBytes(s.value), self.fmtPercent(s.value, total)) ]) ])); + + off -= pct; + }); + + /* The legend is the category list, and when the caller supplies + * opts.onPick it is also how you drill into one. Clicking a category + * was the gesture a user reached for first and it did nothing, which + * is the whole reason this argument exists. */ + var legend = slices.map(function(s) { + var attrs = { 'class': 'af-legend-row' }; + + if (typeof opts.onPick === 'function' && s.pick != null) { + attrs['class'] += ' af-pick'; + attrs.tabindex = '0'; + attrs.role = 'button'; + attrs.title = _('Show only %s').format(s.label); + attrs.click = function(ev) { ev.preventDefault(); opts.onPick(s.pick); }; + /* Keyboard parity: a click target that only responds to a mouse + * is not a control, it is a trap for anyone using a keyboard. */ + attrs.keydown = function(ev) { + if (ev.key === 'Enter' || ev.key === ' ') { + ev.preventDefault(); + opts.onPick(s.pick); + } + }; + } + + return E('li', attrs, [ + E('span', { 'class': 'af-dot', 'style': 'background:' + s.color }), + E('span', { 'class': 'af-legend-lbl', 'title': s.label }, [ s.label ]), + E('span', { 'class': 'af-legend-val af-num' }, [ + self.fmtPercent(s.value, total) + ]) + ]); + }); + + return E('div', { 'class': 'af-donut-wrap' }, [ + E('div', { 'class': 'af-donut' }, [ + this.svg('svg', { 'viewBox': '0 0 42 42', 'role': 'img', + 'aria-label': _('Share of traffic by category') }, ring), + E('div', { 'class': 'af-donut-mid' }, [ + E('strong', {}, [ opts.center || this.fmtBytes(total) ]), + E('span', {}, [ opts.centerLabel || _('total') ]) + ]) + ]), + E('ul', { 'class': 'af-legend' }, legend.length ? legend + : [ E('li', { 'class': 'af-sub' }, [ _('No data') ]) ]) + ]); + }, + + /* Download/upload key shown under the charts. */ + swatches: function(dlText, ulText) { + return E('div', { 'class': 'af-key' }, [ + E('span', {}, [ + E('i', { 'class': 'af-dot af-dl' }), + E('span', { 'class': 'af-sub' }, [ _('Download') ]), + E('strong', { 'class': 'af-num' }, [ dlText ]) + ]), + E('span', {}, [ + E('i', { 'class': 'af-dot af-ul' }), + E('span', { 'class': 'af-sub' }, [ _('Upload') ]), + E('strong', { 'class': 'af-num' }, [ ulText ]) + ]) + ]); + }, + + /* ------------------------------------------------------- empty states */ + + /* Classify an rpc.declare(reject:true) rejection. ubus status 6 is permission + * denied, which means appflowd IS up and the LuCI session lacks the ACL, a + * different problem with a different fix than a stopped daemon (status 4, + * object not found). LuCI encodes the rejection differently across versions + * (a bare number, an Error, or a status string), so probe all three. */ + classifyFail: function(err) { + if (err === 6 || err === '6') + return 'denied'; + var s = (err && typeof err === 'object') ? (err.message || '') : String(err == null ? '' : err); + return (/\b6\b/.test(s) || /permission|denied|access/i.test(s)) ? 'denied' : 'down'; + }, + + notRunning: function(kind, detail) { + var denied = (kind === 'denied'); + + return E('div', { 'class': 'cbi-section af-card af-empty' }, [ + /* Decorative: the

directly below says the same thing in words, + * so this triangle is hidden from assistive tech rather than given + * a name that would be read out twice. */ + this.svg('svg', { 'viewBox': '0 0 24 24', 'aria-hidden': 'true' }, [ + this.svg('path', { + 'd': 'M12 2 1 21h22L12 2zm0 5 7.5 12.9h-15L12 7zm-1 4v5h2v-5h-2zm0 6v2h2v-2h-2z' + }) + ]), + E('h3', {}, [ denied ? _('AppFlow: access denied') : _('appflowd is not running') ]), + E('p', {}, [ denied + ? _('appflowd is running, but this LuCI session was refused access to it over ubus (permission denied). This is an ACL problem, not a stopped service: confirm the luci-app-appflow ACL is installed and reload rpcd.') + : _('The AppFlow collector could not be reached over ubus, so there is no traffic data to show. Start the service and this page picks it up automatically.') + ]), + E('pre', { 'class': 'af-cmd' }, [ denied + ? '/etc/init.d/rpcd restart' + : '/etc/init.d/appflowd enable\n/etc/init.d/appflowd start' + ]), + denied ? '' : E('p', { 'class': 'af-sub' }, [ + _('AppFlow also needs the Netify Agent running (%s), since that is where the per-application classification comes from.').format('/etc/init.d/netifyd status') + ]), + detail ? E('p', { 'class': 'af-sub af-mono' }, [ String(detail) ]) : '' + ]); + }, + + pill: function(text, cls) { + return E('span', { 'class': 'af-pill' + (cls ? ' ' + cls : '') }, [ + cls ? E('i', {}) : '', text + ]); + }, + + /* + * Thin status strip above the cards. `status` is either the full + * appflow.status payload (Overview) or any response envelope, which always + * carries generated_at + agent_connected per §5. + */ + statusStrip: function(status, extra) { + var s = status || {}, + sock = s.socket || {}, + flows = s.flows || {}, + counts = s.aggregates || s.counts || {}, + agent = s.agent || {}, + /* TRI-STATE, deliberately. This used to be + * connected = (s.agent_connected !== false) && (sock.connected !== false) + * which reads an ABSENT field as healthy, because `undefined !== false` + * is true. Overview passes `status || summary`, so whenever the status + * call fails this function is handed the summary payload instead, that + * payload carries neither `agent_connected` nor `socket.connected`, and + * the strip rendered a single green "Netify agent connected" pill. + * + * It failed GREEN on the one indicator whose whole job is to tell the + * operator the collector is down, which is also the most common reason + * the page is empty. Absent evidence is not positive evidence; unknown + * is its own state and is drawn neutral. Found by a code-review panel, + * 2026-08-25. */ + known = (s.agent_connected != null) || (sock.connected != null), + connected = known && (s.agent_connected !== false) && (sock.connected !== false), + bits = [ known + ? this.pill(connected + ? _('Netify agent connected') + : _('Netify agent disconnected'), connected ? 'af-ok' : 'af-warn') + : this.pill(_('Collector state unknown'), 'af-unknown') ]; + + if (flows.tracked != null) + bits.push(this.pill(_('Flows tracked: %d').format(this.num(flows, [ 'tracked' ])))); + + if (counts.apps != null || counts.devices != null) + bits.push(this.pill(_('%d apps, %d devices').format( + this.num(counts, [ 'apps' ]), this.num(counts, [ 'devices' ])))); + + if (s.uptime != null) + bits.push(this.pill(_('Collector uptime: %s').format(this.fmtUptime(s.uptime)))); + + if (agent.version) + bits.push(this.pill(_('netifyd %s').format(String(agent.version)))); + + (extra || []).forEach(function(e) { + if (e) + bits.push(e); + }); + + return E('div', { 'class': 'af-strip' }, bits); + }, + + /* ---------------------------------------------------------------- css */ + + injectCSS: function() { + if (document.getElementById(STYLE_ID)) + return; + + document.head.appendChild(E('style', { + 'id': STYLE_ID, + 'type': 'text/css' + }, [ CSS ])); + } +}); diff --git a/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js b/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js new file mode 100644 index 000000000..5c25ecd1e --- /dev/null +++ b/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js @@ -0,0 +1,679 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require view.appflow.common as appflow'; + +/* + * AppFlow: Overview (live). + * + * DESIGN.md §4 item 1. The live view: total + * throughput, the applications currently moving traffic, the category split + * and the busiest devices, refreshed every 5 s from ubus appflow.summary / + * appflow.devices / appflow.status. + * + * The chart window is kept client-side only: SLOTS samples at INTERVAL + * seconds, i.e. a rolling five minutes that starts over on page load. The + * daemon is not asked to remember anything for it. + */ + +var INTERVAL = 5, + SLOTS = 60, + TOP_APPS = 8, + TOP_DEVICES = 8, + TOP_CATEGORIES = 6, + /* How many application rows to ask the daemon for. The table displays + * TOP_APPS of them; the rest are the filter's raw material, so filtering + * does not fall off the top-N cliff the moment one heavy flow pushes what + * you are looking for out of the visible rows. */ + FETCH_LIMIT = 50; + +function head(cells) { + return E('tr', { 'class': 'tr table-titles' }, cells.map(function(c) { + return E('th', { 'class': 'th' + (c[1] ? ' ' + c[1] : '') }, [ c[0] ]); + })); +} + +return view.extend({ + samples: [], + fails: 0, + painted: false, + + fetch: function() { + var self = this; + + /* Capture the summary rejection (rather than L.resolveDefault-ing it to + * null) so paint can tell a stopped daemon from an ACL denial. */ + return Promise.all([ + /* FETCH_LIMIT, not the daemon's top_n. Filtering a top-10 list to + * "AI" returns nothing as soon as one streaming session pushes AI + * out of the top 10, which looks broken and is worse than no + * filter. The table still displays TOP_APPS rows; the extra rows + * exist so there is something to filter. */ + appflow.rpc.summary(FETCH_LIMIT).then(function(v) { return { v: v }; }, + function(e) { return { e: e }; }), + /* When a device is selected the application list comes from the + * flow table scoped to it, rather than from the global aggregate. */ + L.resolveDefault(self.deviceFilter + ? appflow.rpc.apps('', FETCH_LIMIT, self.deviceFilter.key) + : null, null), + /* The poll has to carry the filter too. A side call that filtered the + * device card was painted over by the very next poll five seconds + * later, so the card flickered back to unfiltered and the feature + * looked dead. Filtering is view state, so every fetch respects it + * rather than one special path doing so. */ + L.resolveDefault((self.filteredKeys && self.filteredKeys.length) + ? appflow.rpc.devices('', TOP_DEVICES, self.filteredKeys) + : appflow.rpc.devices(), null), + L.resolveDefault(appflow.rpc.status(), null) + ]).then(function(r) { + return { summary: r[0].v || null, err: r[0].e, + devApps: r[1], devices: r[2], status: r[3] }; + }); + }, + + load: function() { + /* the icon pack is optional; loadIcons() resolves either way */ + return Promise.all([ appflow.loadIcons(), this.fetch() ]) + .then(function(r) { return r[1]; }); + }, + + /* ------------------------------------------------------------ render */ + + render: function(data) { + appflow.injectCSS(); + + var self = this; + + /* Live filter state. Held on the view rather than read from the DOM so + * that a poll repaint cannot lose what the user typed. */ + this.filter = this.filter || ''; + + /* The selected device, or null. Separate from the text filter because + * they are different kinds of thing: the text is a substring anyone can + * edit, a device is one exact record. */ + this.deviceFilter = this.deviceFilter || null; + + this.devFilter = this.devFilter || ''; + + this.devFilterInput = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'placeholder': _('Filter by device name, MAC or IP'), + 'aria-label': _('Filter by device name, MAC or IP'), + 'value': this.devFilter, + 'input': function(ev) { + self.devFilter = ev.target.value || ''; + if (self.lastDevices) + self.paintDevices(self.lastDevices, self.lastSummary); + } + }); + + this.filterInput = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + 'placeholder': _('Filter by application or category'), + 'aria-label': _('Filter by application or category'), + 'value': this.filter, + 'input': function(ev) { + self.filter = ev.target.value || ''; + if (self.lastSummary) { + self.paintApps(self.lastSummary); + self.refreshDevices(); + } + } + }); + + this.nodes = { + strip: E('div'), + window: E('span'), + kpi: E('div', { 'class': 'af-kpi' }), + chart: E('div'), + apps: E('div'), + appsAux: E('span', {}, [ _('by current rate') ]), + /* Shows which device is selected, with a way out. A filter you + * cannot see is a filter you cannot clear. */ + devChip: E('div', { 'class': 'af-chipbar' }), + cats: E('div'), + /* Its own node so the caption can change when a filter is applied: + * filtered figures come from flows in progress, not cumulative + * totals, and the heading must not keep promising the latter. */ + /* No af-cardaux class: card() wraps whatever it is given in one, and + * passing a second nested one produced the caption twice. */ + devsAux: E('span', {}, [ _('top, by cumulative bytes') ]), + devs: E('div') + }; + + var n = this.nodes; + + this.liveNode = E('div', {}, [ + n.strip, + appflow.card(_('Live throughput'), [ + n.window, + _('%d minute chart, %d second interval').format(SLOTS * INTERVAL / 60, INTERVAL) + ], E('div', {}, [ n.kpi, n.chart ])), + E('div', { 'class': 'af-grid af-split' }, [ + /* card() puts `body` in as a single child, so an ARRAY here lands + * as a nested array that E() does not flatten and nothing after + * the first element renders. Wrap it in one element. Found by + * looking at the live page: the clickable categories worked and + * the filter box was simply absent from the DOM. */ + appflow.card(_('Top applications'), n.appsAux, E('div', {}, [ + E('div', { 'class': 'af-filter' }, [ + this.filterInput, + E('button', { + 'class': 'cbi-button', + 'click': L.bind(function(ev) { + ev.preventDefault(); + this.setFilter(''); + }, this) + }, [ _('Clear') ]) + ]), + /* SAY WHERE THE CATEGORIES ARE, AND SAY WHAT CLEAR DOES. + * + * This read "Click a category below", and the Categories + * panel is beside this card, not below it. A user who looks + * where the text points and finds nothing concludes the + * feature is missing. "Below" was written without checking + * the layout. + * + * It names the panel rather than a direction, because the + * cards wrap underneath each other on a narrow window and + * "to the right" would then be wrong in the other + * direction. */ + n.devChip, + E('div', { 'class': 'af-filter-note' }, [ + _('Start typing an application or category name to show only that traffic, or click a category in the Categories panel. Prefix a word with a minus to exclude it instead, for example -netflix. Press Clear to see all traffic again.') + ]), + n.apps + ])), + appflow.card(_('Categories'), [ _('top, by cumulative bytes') ], n.cats) + ]), + appflow.card(_('Top devices'), n.devsAux, E('div', {}, [ + E('div', { 'class': 'af-filter' }, [ + this.devFilterInput, + E('button', { + 'class': 'cbi-button', + 'click': L.bind(function(ev) { + ev.preventDefault(); + this.setDevFilter(''); + }, this) + }, [ _('Clear') ]) + ]), + E('div', { 'class': 'af-filter-note' }, [ + _('Start typing a device name, MAC address or IP to find it. Click a device to see what it is doing in the applications list above. Press Clear to see all devices again.') + ]), + n.devs + ])) + ]); + + this.downNode = E('div', { 'style': 'display:none' }, [ appflow.notRunning() ]); + + this.paint(data); + + poll.add(L.bind(function() { + return this.fetch().then(L.bind(this.paint, this)); + }, this), INTERVAL); + + return E([], [ + E('h2', {}, [ _('AppFlow') ]), + E('div', { 'class': 'cbi-map-descr' }, [ + _('Live per-application traffic, classified on the router by the Netify Agent. Nothing on this page leaves the device.') + ]), + this.liveNode, + this.downNode + ]); + }, + + /* ------------------------------------------------------------- paint */ + + paint: function(data) { + var summary = data ? data.summary : null, + status = data ? data.status : null; + + if (summary == null) { + this.fails++; + + /* One miss on a live page is usually a blip; only fall back to the + * "not running" screen once it is clearly not coming back. */ + if (this.fails >= (this.painted ? 2 : 1)) + return this.showDown(data ? data.err : null); + + return; + } + + this.fails = 0; + this.painted = true; + this.liveNode.style.display = ''; + this.downNode.style.display = 'none'; + + /* live aggregates carry byte and rate counters in one object; a future + * split into totals/rates is handled by normRates */ + var totals = appflow.normTotals(summary.totals || summary), + rates = summary.rates ? appflow.normRates(summary.rates) : totals; + + /* Stamped so a gap can be measured. The chart still places points by + * INDEX, so the "-5 min" label remains an assumption that every sample + * is INTERVAL apart -- that is a real limitation and is not fixed here. + * What the stamp does fix is the recovery below: without it there was no + * way to tell a 10-second blip from an hour offline, so showDown() + * cleared unconditionally. */ + var nowMs = Date.now(), + last = this.samples.length ? this.samples[this.samples.length - 1].t : 0; + + /* Coming back from a gap longer than the whole window: the old samples + * would be drawn adjacent to the new ones and read as continuous, which + * is a worse lie than an empty chart. Start over. */ + if (last && (nowMs - last) > SLOTS * INTERVAL * 1000) + this.samples = []; + + this.samples.push({ t: nowMs, dl: rates.rdl, ul: rates.rul }); + + while (this.samples.length > SLOTS) + this.samples.shift(); + + var stamp = summary.generated_at || (status ? status.generated_at : 0); + + dom.content(this.nodes.strip, appflow.statusStrip(status || summary, [ + stamp ? E('span', { 'class': 'af-pill' }, [ + _('Updated %s').format(appflow.fmtAgo(stamp)) + ]) : null + ])); + + dom.content(this.nodes.window, [ summary.window + ? _('%d s rate window').format(appflow.num(summary, [ 'window' ])) + : '' ]); + + this.paintThroughput(totals, rates); + /* When a device is selected the application rows come from the + * flow-scoped reply rather than the global aggregate. */ + this.paintApps(summary, data.devApps); + this.paintCategories(summary); + this.paintDevices(data.devices, summary); + this.paintChip(); + }, + + showDown: function(err) { + /* Deliberately NOT clearing this.samples. showDown fires on the SECOND + * consecutive miss, so roughly ten seconds of unavailability used to + * destroy five minutes of history -- and appflowd is restarted by an + * ordinary config change, so that was a hair trigger. paint() now drops + * the buffer only when the gap exceeds the chart's own window, which is + * the case where redrawing it really would mislead. Raised by a + * code-review panel, 2026-08-25. */ + dom.content(this.downNode, appflow.notRunning(appflow.classifyFail(err), err)); + this.liveNode.style.display = 'none'; + this.downNode.style.display = ''; + }, + + /* ---------------------------------------------------------- sections */ + + paintThroughput: function(totals, rates) { + dom.content(this.nodes.kpi, [ + appflow.kpi(_('Download'), appflow.fmtRate(rates.rdl), 'af-dlc'), + appflow.kpi(_('Upload'), appflow.fmtRate(rates.rul), 'af-ulc'), + appflow.kpi(_('Downloaded'), appflow.fmtBytes(totals.dl)), + appflow.kpi(_('Uploaded'), appflow.fmtBytes(totals.ul)) + ]); + + if (this.samples.length < 2) { + dom.content(this.nodes.chart, appflow.placeholder( + _('Collecting samples…'))); + return; + } + + dom.content(this.nodes.chart, [ + appflow.lineChart([ + { values: this.samples.map(function(s) { return s.dl; }), + cls: 'af-dl', fill: true }, + { values: this.samples.map(function(s) { return s.ul; }), + cls: 'af-ul', fill: true } + ], { + slots: SLOTS, + height: 170, + fmt: function(v) { return appflow.fmtRate(v); }, + xlabels: [ _('-%d min').format(SLOTS * INTERVAL / 60), _('now') ] + }), + appflow.swatches(appflow.fmtRate(this.samples[this.samples.length - 1].dl), + appflow.fmtRate(this.samples[this.samples.length - 1].ul)) + ]); + }, + + /* Set the filter box and repaint. Used by the category legend, so clicking + * a category is exactly equivalent to typing its name: the user can see + * what was applied, edit it, or clear it. One mechanism, not two. */ + setDevFilter: function(text) { + this.devFilter = text || ''; + + if (this.devFilterInput) + this.devFilterInput.value = this.devFilter; + + /* Clearing the box also drops the application scope, because the two + * were set by one click and leaving half of it behind is how a user + * ends up looking at a filtered list with nothing saying why. */ + if (!this.devFilter && this.deviceFilter) + return this.setDevice(null); + + if (this.lastDevices) + this.paintDevices(this.lastDevices, this.lastSummary); + }, + + /* Select a device, or pass null to clear. Repaints from the live data we + * already hold, then refetches so the application list is scoped. */ + setDevice: function(key, name) { + this.deviceFilter = key ? { key: key, name: name || key } : null; + + /* Fill the device box too, so the list narrows to what was selected. + * Clicking a device used to scope the application list while the device + * list carried on showing everything, so the thing just selected was + * not visibly selected anywhere. */ + this.devFilter = key ? (name || key) : ''; + if (this.devFilterInput) + this.devFilterInput.value = this.devFilter; + + this.paintChip(); + this.refetch(); + }, + + paintChip: function() { + var self = this, d = this.deviceFilter; + + if (!this.nodes || !this.nodes.devChip) + return; + + dom.content(this.nodes.devChip, d ? [ + E('span', { 'class': 'af-chip' }, [ + E('span', {}, [ _('Device: %s').format(d.name) ]), + E('a', { + 'href': '#', + 'class': 'af-chip-x', + 'title': _('Show all devices'), + 'click': function(ev) { ev.preventDefault(); self.setDevice(null); } + }, [ '\u00d7' ]) + ]) + ] : []); + }, + + /* One refetch path for both filters, so a repaint cannot lose either. */ + refetch: function() { + var self = this; + + return this.fetch().then(function(d) { self.paint(d); }); + }, + + setFilter: function(text) { + this.filter = text || ''; + if (this.filterInput) + this.filterInput.value = this.filter; + if (this.lastSummary) { + this.paintApps(this.lastSummary); + this.refreshDevices(); + } + }, + + /* Ask the daemon for device totals restricted to the filtered applications. + * paintApps() must have run first, because it is what computes which keys + * survived. + * + * Failure is deliberately quiet: the device card keeps showing the + * unfiltered view rather than emptying itself, because a filter that blanks + * an unrelated card looks like a crash. */ + refreshDevices: function() { + var self = this, + keys = this.filteredKeys; + + var call = (keys && keys.length) + /* '' and not null for `sort`. The ubus policy types it as a string, and + * a null fails the policy with code 2 (Invalid argument) rather than + * being treated as absent. resolveDefault below then swallowed the + * rejection and the card silently kept its unfiltered contents, which + * is how this bug hid: the feature looked implemented and did + * nothing. */ + ? appflow.rpc.devices('', TOP_DEVICES, keys) + : appflow.rpc.devices(); + + return L.resolveDefault(call, null).then(function(d) { + if (d && self.lastSummary) + self.paintDevices(d, self.lastSummary); + }); + }, + + paintApps: function(summary, devApps) { + var self = this; + this.lastSummary = summary; + + if (devApps !== undefined) + this.lastDevApps = devApps; + + /* Device-scoped rows when a device is selected, otherwise the global + * aggregate. Both go through the same normalisation and the same text + * filter below, so the two filters compose. */ + var scoped = this.deviceFilter ? (this.lastDevApps || null) : null; + + if (this.nodes && this.nodes.appsAux) + dom.content(this.nodes.appsAux, [ scoped + ? _('on %s, from flows in progress').format(this.deviceFilter.name) + : _('by current rate') ]); + + var terms = appflow.parseFilter(this.filter); + + /* Filter BEFORE the top-N slice, or the filter only ever searches the + * ten rows that were already visible. */ + var all = appflow.toList(scoped + ? (scoped.apps || []) + : (summary.top_apps || summary.apps)) + .map(function(a) { return appflow.normApp(a); }) + .filter(function(a) { + return appflow.matchFilter(terms, + [ a.label, a.key, appflow.catLabel(a.cat) ]); + }) + /* Scoped rows have no rate, so sorting by it would order them all + * equal and the top-N slice would be arbitrary. */ + .sort(scoped + ? function(a, b) { return b.total - a.total; } + : function(a, b) { return b.rate - a.rate; }); + + /* Every key that survived the filter, not just the displayed rows: the + * device query should account for all matching traffic, not only the + * top eight of it. */ + this.filteredKeys = terms.length + ? all.map(function(a) { return a.key; }).filter(function(k) { return k; }) + : null; + + var apps = all.slice(0, TOP_APPS); + + if (!apps.length) { + dom.content(this.nodes.apps, appflow.placeholder(terms.length + ? _('No application matches that filter.') + : _('No application traffic in the current window.'))); + return; + } + + /* UNITS CHANGE WITH THE SOURCE, AND SO MUST THE HEADINGS. + * + * The global aggregate carries rate rings, so the unscoped table shows + * rates. The device-scoped rows are summed from the live flow table, + * which holds cumulative per-flow counters and no rate ring, so there + * is no rate to show. Rendering them through fmtRate produced a table + * of zeroes: every value present, every one formatted from a field that + * does not exist. + * + * Computing a per-device-per-application rate would need a delta ring + * per pair, which is the new state this whole approach avoids. Showing + * bytes is the honest answer, and the column headings say which. */ + var byBytes = !!scoped, + fmt = byBytes ? appflow.fmtBytes : appflow.fmtRate, + val = byBytes + ? function(a) { return { dl: a.dl, ul: a.ul }; } + : function(a) { return { dl: a.rdl, ul: a.rul }; }; + + var peak = byBytes + ? (apps[0].total || 1) + : (apps[0].rate || 1); + + dom.content(this.nodes.apps, E('table', { 'class': 'table af-table' }, [ + head([ + [ _('Application') ], + [ '' ], + [ byBytes ? _('Downloaded') : _('Download'), 'af-num' ], + [ byBytes ? _('Uploaded') : _('Upload'), 'af-num' ] + ]) + ].concat(apps.map(function(a) { + var v = val(a); + + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [ appflow.appCell(a, null, function(x) { + self.setFilter(x); + }) ]), + E('td', { 'class': 'td', 'style': 'width:26%' }, [ + appflow.bar(v.dl, v.ul, peak, fmt) + ]), + E('td', { 'class': 'td af-num' }, [ fmt(v.dl) ]), + E('td', { 'class': 'td af-num' }, [ fmt(v.ul) ]) + ]); + })))); + }, + + paintCategories: function(summary) { + var cats = appflow.toList(summary.top_categories || summary.categories, 'category') + .map(function(c) { + var t = appflow.normTotals(c); + + return { + label: appflow.catLabel(appflow.str(c, + [ 'category', 'category_name', 'cat', 'key', 'name', 'label' ])), + key: appflow.str(c, + [ 'category', 'category_name', 'cat', 'key', 'name', 'label' ]), + /* Cumulative bytes ONLY. This used to be `t.total || t.rate`, + * so a category carrying a rate but a zero/absent byte total + * contributed a RATE into a sum of byte totals, which the + * doughnut centre then printed through fmtBytes under a card + * caption reading "top, by cumulative bytes". The fallback + * looked like belt-and-braces and was actually a silent unit + * mix; a row with no bytes is filtered out below, which is + * the honest outcome. Found by a code-review panel. */ + value: t.total + }; + }) + .filter(function(c) { return c.value > 0; }) + .sort(function(a, b) { return b.value - a.value; }); + + if (!cats.length) { + dom.content(this.nodes.cats, appflow.placeholder(_('No categories yet.'))); + return; + } + + var slices = cats.slice(0, TOP_CATEGORIES).map(function(c) { + /* `pick` is what clicking this slice types into the filter box. The + * LABEL, not the key, because the filter matches on the rendered + * category label and the user can then read and edit it. */ + return { label: c.label, value: c.value, pick: c.label, + color: appflow.color(c.key) }; + }); + + if (cats.length > TOP_CATEGORIES) { + var rest = cats.slice(TOP_CATEGORIES).reduce(function(a, c) { + return a + c.value; + }, 0); + + if (rest > 0) + slices.push({ label: _('Other'), value: rest, color: appflow.color('other') }); + } + + var self = this; + + dom.content(this.nodes.cats, appflow.donut(slices, { + centerLabel: _('total'), + /* "Other" is this view's roll-up of everything past the top N, not + * a category anything is tagged with, so filtering by it would + * match nothing. It is left unclickable rather than offered and + * broken. */ + onPick: function(pick) { + if (pick !== _('Other')) + self.setFilter(pick); + } + })); + }, + + paintDevices: function(devices, summary) { + var self = this; + /* The daemon says whether it answered from the live flow table. When it + * did, these are the bytes currently-tracked flows account for, NOT + * lifetime device totals, and the caption has to say so. */ + var fromFlows = !!(devices && devices.from_flows); + + if (this.nodes.devsAux) + dom.content(this.nodes.devsAux, [ fromFlows + ? _('matching the filter, from flows in progress') + : _('by cumulative bytes') ]); + + var list = appflow.toList( + (devices && (devices.devices || devices.clients)) || devices || summary.top_devices, + 'mac'); + + this.lastDevices = devices; + + var devTerms = appflow.parseFilter(this.devFilter); + + var devs = list + .map(function(d) { return appflow.normDevice(d); }) + .filter(function(d) { return d.total > 0 || d.rate > 0; }) + /* Same matcher as the application filter, so -exclude works here + * too and the two boxes behave identically. */ + .filter(function(d) { + return appflow.matchFilter(devTerms, [ d.name, d.mac, d.ip, d.key ]); + }) + /* Cumulative bytes first, rate only as the tie-break. These were the + * other way round until 2026-08-25, which disagreed with this card's + * own caption ("top, by cumulative bytes") and with the bar beside + * each row, which is scaled against max(total) below. A table + * ordered by rate and drawn against a cumulative scale produces bars + * that do not shorten down the column, directly under a Top + * applications table that does -- both visible in one screenshot. + * + * It also chose the wrong rows: most devices sit at rate 0 at any + * instant, so a phone doing a 2 KB/s keepalive outranked a TV that + * pulled 12 GB an hour ago, and with enough devices trickling the TV + * left the card entirely. Found by a security/first-look review of + * this file, which had never been reviewed by anyone. */ + .sort(function(a, b) { return (b.total - a.total) || (b.rate - a.rate); }) + .slice(0, TOP_DEVICES); + + if (!devs.length) { + dom.content(this.nodes.devs, appflow.placeholder(devTerms.length + ? _('No device matches that filter.') + : _('No active devices.'))); + return; + } + + var peak = devs.reduce(function(m, d) { + return Math.max(m, d.total); + }, 0) || 1; + + dom.content(this.nodes.devs, E('table', { 'class': 'table af-table' }, [ + head([ + [ _('Device') ], + [ '' ], + [ _('Current rate'), 'af-num' ], + [ _('Downloaded'), 'af-num' ], + [ _('Uploaded'), 'af-num' ] + ]) + ].concat(devs.map(function(d) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'width:32%' }, [ + appflow.deviceCell(d, function(dev) { self.setDevice(dev.key, dev.name); }) + ]), + E('td', { 'class': 'td', 'style': 'width:26%' }, [ + appflow.bar(d.dl, d.ul, peak) + ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtRate(d.rate) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(d.dl) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(d.ul) ]) + ]); + })))); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js b/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js new file mode 100644 index 000000000..d99ead819 --- /dev/null +++ b/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js @@ -0,0 +1,577 @@ +'use strict'; +'require view'; +'require dom'; +'require poll'; +'require ui'; +'require view.appflow.common as appflow'; + +/* + * AppFlow: Statistics (past hour). + * + * DESIGN.md §4 item 2: the range view, served from the daemon's + * in-RAM 12 x 5-minute buckets via ubus appflow.stats {range:"hour"}. Layout, + * column set, sort order, the synthetic "All traffic" row, the search box, the + * per-app detail drawer and the 15 s poll follow the conventions of the + * "Flow Control -> Data Statistics" screen. + * + * Past day / past week are phase 2 (they need persistence). The tabs are shown + * disabled rather than hidden, so the seam is visible instead of pretended away. + */ + +var INTERVAL = 15, + TOP_N = 10, + MAX_ROWS = 200, + RANGE = 'hour'; + +var COLUMNS = [ + { key: 'label', title: _('Application'), cls: '' }, + { key: 'dl', title: _('Download'), cls: 'af-num' }, + { key: 'ul', title: _('Upload'), cls: 'af-num' }, + { key: 'total', title: _('Total'), cls: 'af-num' } +]; + +return view.extend({ + sortKey: 'total', + sortDir: -1, + filter: '', + fails: 0, + painted: false, + stats: null, + + fetch: function() { + /* Keep the rejection (via this.lastErr) instead of L.resolveDefault-ing + * it away, so showDown can tell a stopped daemon from an ACL denial. */ + var self = this; + return appflow.rpc.stats(RANGE).then( + function(v) { self.lastErr = null; return v; }, + function(e) { self.lastErr = e; return null; } + ); + }, + + load: function() { + /* the icon pack is optional; loadIcons() resolves either way */ + return Promise.all([ appflow.loadIcons(), this.fetch() ]) + .then(function(r) { return r[1]; }); + }, + + /* ------------------------------------------------------------ render */ + + render: function(stats) { + appflow.injectCSS(); + + this.nodes = { + strip: E('div'), + top: E('div'), + rows: E('div'), + count: E('span', { 'class': 'af-sub' }) + }; + + var tip = _('Requires history storage (planned)'); + + this.searchInput = E('input', { + 'type': 'text', + 'class': 'cbi-input-text', + /* It has always matched the CATEGORY label as well as the app name. + * Saying "Search app name" meant nobody could discover that, which + * is closer to a defect than a wording nit: the capability shipped + * and the label concealed it. */ + 'placeholder': _('Filter by application or category'), + 'aria-label': _('Filter by application or category'), + 'input': L.bind(function(ev) { + this.filter = (ev.target.value || '').toLowerCase(); + this.paintTable(); + }, this) + }); + + this.clearButton = E('button', { + 'class': 'cbi-button cbi-button-negative', + 'click': L.bind(this.handleClear, this) + }, [ _('Clear') ]); + + this.liveNode = E('div', {}, [ + E('ul', { 'class': 'cbi-tabmenu af-ranges' }, [ + E('li', { 'class': 'cbi-tab' }, [ + E('a', { 'href': '#', 'click': function(ev) { ev.preventDefault(); } }, + [ _('Past hour') ]) + ]), + E('li', { 'class': 'cbi-tab-disabled af-disabled', 'title': tip }, [ + E('a', { 'title': tip, 'click': function(ev) { ev.preventDefault(); } }, + [ _('Past day') ]) + ]), + E('li', { 'class': 'cbi-tab-disabled af-disabled', 'title': tip }, [ + E('a', { 'title': tip, 'click': function(ev) { ev.preventDefault(); } }, + [ _('Past week') ]) + ]) + ]), + this.nodes.strip, + appflow.card(_('Top 10 apps by bandwidth usage'), + [ _('last 60 minutes') ], this.nodes.top), + appflow.card(_('App traffic statistics'), null, E('div', {}, [ + E('div', { 'class': 'af-toolbar' }, [ + this.searchInput, + this.nodes.count, + E('div', { 'class': 'af-spacer' }), + this.clearButton + ]), + this.nodes.rows + ])) + ]); + + this.downNode = E('div', { 'style': 'display:none' }, [ appflow.notRunning() ]); + + this.paint(stats); + + poll.add(L.bind(function() { + return this.fetch().then(L.bind(this.paint, this)); + }, this), INTERVAL); + + return E([], [ + E('h2', {}, [ _('AppFlow') ]), + E('div', { 'class': 'cbi-map-descr' }, [ + _('Per-application totals for the last hour, aggregated on the router from the Netify Agent\'s classification. Select a row for the per-device breakdown.') + ]), + this.liveNode, + this.downNode + ]); + }, + + /* ------------------------------------------------------------- paint */ + + paint: function(stats) { + if (stats == null) { + this.fails++; + + if (this.fails >= (this.painted ? 2 : 1)) + return this.showDown(); + + return; + } + + this.fails = 0; + this.painted = true; + this.stats = stats; + this.liveNode.style.display = ''; + this.downNode.style.display = 'none'; + this.clearButton.disabled = false; + + dom.content(this.nodes.strip, appflow.statusStrip(stats, [ + stats.generated_at ? E('span', { 'class': 'af-pill' }, [ + _('Updated %s').format(appflow.fmtAgo(stats.generated_at)) + ]) : null, + appflow.pill(_('12 buckets of 5 min')) + ])); + + this.paintTop(); + this.paintTable(); + }, + + showDown: function() { + this.stats = null; + dom.content(this.downNode, appflow.notRunning(appflow.classifyFail(this.lastErr), this.lastErr)); + this.liveNode.style.display = 'none'; + this.downNode.style.display = ''; + }, + + /* Applications, normalised and sorted; "All traffic" is kept separate. */ + appList: function() { + var stats = this.stats || {}; + + return appflow.toList(stats.applications || stats.apps) + .map(function(a) { return appflow.normApp(a); }) + .filter(function(a) { + /* the daemon may already include the synthetic row */ + return a.key != '-1' && a.key != 'all_traffic'; + }); + }, + + allRow: function(apps) { + var stats = this.stats || {}; + + if (stats.all != null) + return appflow.normTotals(stats.all); + + return apps.reduce(function(a, x) { + a.dl += x.dl; + a.ul += x.ul; + a.total += x.total; + + return a; + }, { dl: 0, ul: 0, total: 0 }); + }, + + /* ---------------------------------------------------------- top ten */ + + paintTop: function() { + var stats = this.stats || {}, + top = appflow.toList(stats.top_apps); + + if (!top.length) + top = this.appList().sort(function(a, b) { return b.total - a.total; }); + else + top = top.map(function(a) { return appflow.normApp(a); }) + .sort(function(a, b) { return b.total - a.total; }); + + top = top.slice(0, TOP_N); + + if (!top.length) { + dom.content(this.nodes.top, appflow.placeholder( + _('No application traffic recorded in the last hour.'))); + return; + } + + var peak = appflow.num(stats, [ 'max_bytes' ], 0) || top[0].total || 1, + self = this; + + dom.content(this.nodes.top, [ + E('table', { 'class': 'table af-table' }, top.map(function(a) { + return E('tr', { + 'class': 'tr af-row', + 'title': _('Show per-device details'), + 'click': L.bind(self.handleDetail, self, a) + }, [ + E('td', { 'class': 'td', 'style': 'width:28%' }, [ appflow.appCell(a) ]), + E('td', { 'class': 'td' }, [ appflow.bar(a.dl, a.ul, peak) ]), + E('td', { 'class': 'td af-num', 'style': 'width:14%' }, [ + appflow.fmtBytes(a.total) + ]) + ]); + })), + appflow.swatches( + appflow.fmtBytes(top.reduce(function(s, a) { return s + a.dl; }, 0)), + appflow.fmtBytes(top.reduce(function(s, a) { return s + a.ul; }, 0))) + ]); + }, + + /* ------------------------------------------------------- main table */ + + handleSort: function(key) { + if (this.sortKey == key) + this.sortDir = -this.sortDir; + else { + this.sortKey = key; + this.sortDir = (key == 'label') ? 1 : -1; + } + + this.paintTable(); + }, + + paintTable: function() { + var self = this, + apps = this.appList(), + all = this.allRow(apps), + total = apps.length; + + /* Shared with the overview via common.js, so the two pages cannot drift + * and the logic is reachable by a Node test. Adds exclusion: a term + * prefixed with a minus removes matching rows, which is what you want + * when one heavy application is burying everything else. */ + var terms = appflow.parseFilter(this.filter); + + if (terms.length) + apps = apps.filter(function(a) { + return appflow.matchFilter(terms, + [ a.label, a.key, appflow.catLabel(a.cat) ]); + }); + + apps.sort(function(a, b) { + var k = self.sortKey; + + if (k == 'label') + return self.sortDir * a.label.localeCompare(b.label); + + return self.sortDir * ((a[k] - b[k]) || (a.total - b.total)); + }); + + dom.content(this.nodes.count, [ this.filter.length + ? _('%d of %d applications').format(apps.length, total) + : _('%d applications').format(total) ]); + + var truncated = apps.length > MAX_ROWS; + + if (truncated) + apps = apps.slice(0, MAX_ROWS); + + var header = E('tr', { 'class': 'tr table-titles' }, COLUMNS.map(function(c) { + return E('th', { + 'class': 'th af-sortable ' + c.cls, + 'title': _('Sort by %s').format(c.title), + 'click': L.bind(self.handleSort, self, c.key) + }, [ + c.title, + (self.sortKey == c.key) + ? E('span', { 'class': 'af-caret' }, [ self.sortDir < 0 ? '▼' : '▲' ]) + : '' + ]); + })); + + var rows = [ header ]; + + /* Synthetic "All traffic" summary row, always pinned to the top. */ + rows.push(E('tr', { 'class': 'tr af-all' }, [ + E('td', { 'class': 'td' }, [ + E('span', {}, [ _('All traffic') ]), + E('span', { 'class': 'af-sub' }, [ _('sum of all applications') ]) + ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(all.dl) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(all.ul) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(all.total) ]) + ])); + + if (!apps.length) + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'colspan': String(COLUMNS.length) }, [ + appflow.placeholder(this.filter.length + ? _('No application matches "%s".').format(this.filter) + : _('No application traffic recorded in the last hour.')) + ]) + ])); + + apps.forEach(function(a) { + /* Keyboard-reachable, not mouse-only. The per-device drawer is the + * feature this whole view exists for and until 2026-08-25 a `click` + * handler was the only way in: no tabindex, no role, no key + * handler anywhere in this package. role=button plus tabindex puts + * the row in the tab order and makes it announce as activatable; + * Enter and Space are the two keys a button is required to answer. + * Raised by a code-review panel. */ + rows.push(E('tr', { + 'class': 'tr af-row', + 'title': _('Show per-device details'), + 'role': 'button', + 'tabindex': '0', + 'aria-label': _('Show per-device details for %s').format(a.label || a.key), + 'keydown': L.bind(function(app, ev) { + if (ev.key !== 'Enter' && ev.key !== ' ' && ev.key !== 'Spacebar') + return; + ev.preventDefault(); /* Space would scroll the page */ + self.handleDetail(app, ev); + }, self, a), + 'click': L.bind(self.handleDetail, self, a) + }, [ + E('td', { 'class': 'td' }, [ appflow.appCell(a) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(a.dl) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(a.ul) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(a.total) ]) + ])); + }); + + dom.content(this.nodes.rows, [ + E('table', { 'class': 'table af-table' }, rows), + truncated ? E('div', { 'class': 'af-placeholder' }, [ + _('Showing the first %d applications; narrow the search to see the rest.') + .format(MAX_ROWS) + ]) : '' + ]); + }, + + /* ------------------------------------------------------ detail drawer */ + + handleDetail: function(app, ev) { + if (ev) + ev.stopPropagation(); + + /* Mark which app this drawer is for. A late detail() result only renders + * if it still matches; today the spinner overlay blocks a second row + * click while one is in flight, but that is an accident of the overlay + * being modal, so the guard should not depend on it. */ + this.detailKey = app.key; + + // Title must be a node array, never a bare string: LuCI's showModal + // routes a bare-string title through innerHTML, and app.label aliases + // to attacker-influenced fields (DHCP hostname, TLS SNI) one edit away. + ui.showModal([ app.label ], [ + E('p', { 'class': 'spinning' }, [ _('Collecting data…') ]) + ], 'cbi-modal'); + + appflow.rpc.detail(app.key).then(L.bind(function(detail) { + this.renderDetail(app, detail); + }, this)).catch(L.bind(function(err) { + this.renderDetail(app, null, err); + }, this)); + }, + + renderDetail: function(app, detail, err) { + /* the drawer may have been dismissed while the call was in flight */ + if (!document.body.classList.contains('modal-overlay-active')) + return; + + /* or replaced by a drawer for a different app: a late result for the + * previously-open app must not overwrite the one now showing */ + if (this.detailKey !== app.key) + return; + + var d = detail || {}, + totals = appflow.normTotals(d.metrics || d.totals || d), + /* Do not re-normalise app.series: normApp() already did. The alias + * fix in common.js makes that harmless now, but normalising once is + * the actual intent and it keeps the fallback readable. */ + series = appflow.normSeries(d.time_series || d.series), + devices = appflow.toList(d.devices || d.clients || d.mac_addresses, 'mac') + .map(function(x) { return appflow.normDevice(x); }) + .filter(function(x) { return x.total > 0; }) + .sort(function(a, b) { return b.total - a.total; }), + hosts = appflow.toList(d.hostnames || d.hosts, 'hostname') + .map(function(h) { + var t = appflow.normTotals(h); + + return { + name: appflow.str(h, [ 'hostname', 'host', 'name', 'domain' ], '?'), + total: t.total + }; + }) + .filter(function(h) { return h.total > 0; }) + .sort(function(a, b) { return b.total - a.total; }) + .slice(0, 8); + + if (!totals.total) + totals = { dl: app.dl, ul: app.ul, total: app.total }; + + var body = [ + E('div', { 'class': 'af-modal-head' }, [ + appflow.tile(app.label, app.cat, app.tag), + E('div', {}, [ + E('h4', { 'style': 'display:block;text-align:start;padding:0;margin:0' }, [ app.label ]), + app.cat ? E('span', { + 'class': 'af-badge', + 'style': 'background:' + appflow.color(app.cat) + }, [ appflow.catLabel(app.cat) ]) : E('span', { 'class': 'af-sub' }, [ + _('Unclassified') + ]) + ]) + ]), + E('div', { 'class': 'af-kpi' }, [ + appflow.kpi(_('Download'), appflow.fmtBytes(totals.dl), 'af-dlc'), + appflow.kpi(_('Upload'), appflow.fmtBytes(totals.ul), 'af-ulc'), + appflow.kpi(_('Total'), appflow.fmtBytes(totals.total)), + appflow.kpi(_('Devices'), String(devices.length)) + ]) + ]; + + if (err) + body.push(E('div', { 'class': 'alert-message warning' }, [ + _('The daemon could not return details for this application: %s') + .format(String(err.message || err)) + ])); + else if (detail && detail.found === false) + body.push(E('div', { 'class': 'alert-message warning' }, [ + _('This application is no longer in the daemon\'s aggregate table; it may have been pruned since the table was last refreshed.') + ])); + + body.push(E('h4', {}, [ _('Data usage') ])); + + if (series.length) + body.push(appflow.columnChart(series, { + height: 150, + /* The axis is labelled -60 min, so it must span the daemon's full + * trailing hour regardless of how many buckets have accumulated + * so far. That hour is TWELVE five-minute buckets + * (appflowd: SERIES_SLOTS=12, SERIES_MS=300000), not sixty -- + * passing 60 here would squeeze a complete hour into the last + * fifth of the chart, which is a worse lie than the one this + * fixes. If SERIES_SLOTS changes, this changes with it. */ + slots: 12, + xlabels: [ _('-%d min').format(60), _('now') ] + })); + else + body.push(appflow.placeholder(_('No time series available for this application.'))); + + body.push(E('h4', {}, [ _('Devices') ])); + + if (devices.length) { + var rows = [ E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, [ _('Client') ]), + E('th', { 'class': 'th' }, [ _('Last seen') ]), + E('th', { 'class': 'th af-num' }, [ _('Download') ]), + E('th', { 'class': 'th af-num' }, [ _('Upload') ]), + E('th', { 'class': 'th af-num' }, [ _('Share') ]) + ]) ]; + + devices.forEach(function(dev) { + rows.push(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, [ appflow.deviceCell(dev) ]), + E('td', { 'class': 'td af-sub' }, [ appflow.fmtAgo(dev.seen) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(dev.dl) ]), + E('td', { 'class': 'td af-num' }, [ appflow.fmtBytes(dev.ul) ]), + E('td', { 'class': 'td af-num' }, [ + appflow.fmtPercent(dev.total, totals.total) + ]) + ])); + }); + + body.push(E('table', { 'class': 'table af-table' }, rows)); + } + else { + body.push(appflow.placeholder( + _('No per-device breakdown available for this application.'))); + } + + if (hosts.length) { + body.push(E('h4', {}, [ _('Hosts') ])); + body.push(E('ul', { 'class': 'af-legend' }, hosts.map(function(h) { + return E('li', {}, [ + E('span', { 'class': 'af-legend-lbl af-mono', 'title': h.name }, [ h.name ]), + E('span', { 'class': 'af-legend-val af-num' }, [ + appflow.fmtBytes(h.total) + ]) + ]); + }))); + } + + body.push(E('div', { 'class': 'af-btnrow' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'click': ui.hideModal + }, [ _('Close') ]) + ])); + + /* Node array, not a bare string -- the same rule stated at the top of + * handleDetail() and, until 2026-08-26, not followed here. LuCI routes a + * bare-string title through innerHTML, and `_()` returns whatever the + * active catalog says. With the English catalog that is a constant in + * this file; the moment a translation is accepted it is third-party + * content, which is exactly what the pending zh_Hans PR proposes. Raised + * by a review panel over that PR, which was careful to note the submitted + * Chinese contains no markup: this is closing the trust distinction, not + * fixing an exploit. */ + ui.showModal([ _('Details') ], body, 'cbi-modal'); + }, + + /* -------------------------------------------------------------- clear */ + + handleClear: function() { + ui.showModal([ _('Clear statistics') ], [ + E('p', {}, [ + _('This resets the per-application, per-device and per-category counters and time buckets. Live flow tracking continues uninterrupted, so the figures on this page simply start again from zero.') + ]), + E('div', { 'class': 'af-btnrow' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'click': ui.hideModal + }, [ _('Cancel') ]), + E('button', { + 'class': 'btn cbi-button cbi-button-negative important', + 'click': ui.createHandlerFn(this, 'handleClearConfirm') + }, [ _('Clear') ]) + ]) + ]); + }, + + handleClearConfirm: function() { + return appflow.rpc.reset().then(L.bind(function() { + ui.hideModal(); + ui.addTimeLimitedNotification(null, + E('p', {}, [ _('AppFlow statistics cleared.') ]), 4000, 'info'); + + return this.fetch().then(L.bind(this.paint, this)); + }, this), function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, [ + _('Could not clear the statistics: %s').format(String(err.message || err)) + ]), 'error'); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-appflow/icons/CHECKSUMS.sha256 b/luci-app-appflow/icons/CHECKSUMS.sha256 new file mode 100644 index 000000000..c2b5a571d --- /dev/null +++ b/luci-app-appflow/icons/CHECKSUMS.sha256 @@ -0,0 +1,96 @@ +c27280899abed89c01aac44352c050fc214030d26ab9bac7dd29a82ac5b968b6 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__adobe.svg +3ab3e2114a4e7fca84f2da92f8ab982d9d6d304d049db0dafbd9c3f2b0e44801 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon-web-services.svg +326fd7c65f787c62b7c509392b07f58e132cd1de75f349f97337f4df668077aa *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon.svg +a09cf032b7615925cf81f44d1f33a3eaaa783c8e087073179012d8a1008dec33 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__bing.svg +3d5cd47a37f09f7984ee6eab149ac94c5c5dc4e88719af11f37cfc25fd41075f *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__crowdstrike.svg +cdc480ea45b4fe48c3503e118d763f959ec9affdb645c1a71467b24b108a15ee *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__disney-plus.svg +b1edb8d6ffa25fa030d9dbba46f0f686e6873d3d35b5f4cddc73dc913823b7e1 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__hulu.svg +67f08e714e9a8a06288f1d7adc7e9840c450c11f8af6fb1847425fc7f186ca1b *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__linkedin.svg +197fb9f9c83c1e75c4887f6a243521a7703095623614f250b040484d481cbcb8 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-onedrive.svg +d778548786d06a8ddd87ff43375df55504ee55473b683cede178f3308f00257d *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-windows.svg +2e3fc3da6302a899939c1154f7f82d69266d6e55348163e8121129b07f46cd5c *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft.svg +5643dc19834575e072fee8d691d240adc667b1fd1509be86d6712373155fb3c0 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__oracle.svg +6356430254ecece9e747a5505176caeb212e991240b1dd905486455b66db86b5 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__salesforce.svg +4c310179f4e30ad8a4fc663be5b9122b2b0284c5b5b2e581c0230d8534fc16da *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__skype.svg +62e556a75b94516fd8dcfa9c8ee4eae76268b4087ee927d91a5f2d3115d54118 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__slack.svg +1dfadc3006dede2a758a9c4c4e407ea42aadb70d337b86eb565496003f5c03a9 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__sophos.svg +dc89ec954c80ae26fe52b07b194113bd9caecfbd67dc120bd1ae6a29f3386243 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__twitter.svg +2e693dca0511b36fa4a0b20a099d4c7c4b5b4fd2f2991e3bfbcc49d09d8ca796 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__weibo.svg +f12d88f5a0fab740b6dd53081531435c66fe5bdace51a9f4a22ef1aa54e7a539 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__xbox.svg +f742c7c3c4ade43833f5f9d1768db1fbd3693baaf95799010e17fcedfd355386 *root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__yahoo.svg +04afb2a1db3f619be9030b38fd43440034a58db16adc8fed49de87b90b9e2778 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__airbnb.svg +8049dde7772116e573864e0d3fab1fd5078c75f88e90dc01f70f63a789889a1a *root/www/luci-static/resources/appflow-icons/svg/simple-icons__alipay.svg +71eddad858b255223a196ef07c4b5744e74f95d21ca2b7413ff915aa0d2c9173 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__avast.svg +e2eeaca4c5273ecff775ee4b357091678a7587b2d606190f8e5b7dce4d93a781 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__baidu.svg +4b86a7be3d81cd7484b3a1fdaebe346f99040b6212ebb8ec059e55a8a43ef2b6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__bitdefender.svg +408096d99907fd99e0863cb5d73f77a72537c6a85fd8d5e2704178a83a891f08 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__cloudflare.svg +94dd360e4a750ce3a902c8bd7c83d9db8078bf923e4a040b538d92e0967a9d4d *root/www/luci-static/resources/appflow-icons/svg/simple-icons__coinbase.svg +19d9da58d96ee8bd05bf7253f706203dd0c1762469523b8960eb8efc3b1017ef *root/www/luci-static/resources/appflow-icons/svg/simple-icons__dazn.svg +1d364b72c9eaf1fe37d17ca88cd8fb541308dc0f3b09e2ab3b824f380b3493d5 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__discord.svg +65571291c261ef869c31540a38f3ca0f0ee3bb73f179c172211c8860f19a6359 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__docker.svg +1c9ea4ac8e318f3f6a598568df1a38ece03b7bc52a3a72d48c9e760340d7aeec *root/www/luci-static/resources/appflow-icons/svg/simple-icons__dropbox.svg +846e8d8ac6cea49766e7c62e095e739d115ed355f9b63bf46067f14c9343c745 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__ebay.svg +a19b1eb5a46edc11a7dc7f1ce6fa1701ea4e4cf451feec88441c523f4b50cde3 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__epicgames.svg +b06d18d844ed621b89faffb1a33440cc0ec4f1ffea9f36191f50db19a47c59a6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__facebook.svg +c814b5b85f8dd650c3be063f5ea2e264cf3242ef7f0c91caf6f3393ebf73e654 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__fifa.svg +819fc94c6ce1dfa1874f069c88ff9d666fb4ce669e23781ec8dbf163a5a012e2 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__firefox.svg +8bca3e50e008b210c12667b5443ef7f63274f50b229c6bd61f5653a53349fca4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__fitbit.svg +3bf8cceead820aec50d4ee825a3fd02c5a1cd6665cc9cf4cbf3d9c8861a204bb *root/www/luci-static/resources/appflow-icons/svg/simple-icons__github.svg +c7c39058bd1b6f9f40334383bd5136bb8c5ba5e6a24200f2d1f18365e2526e28 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__gitlab.svg +729bf1f97587dd60e7592a98993543fec7ec3583a907a0e54b9fd88f7f28726f *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleads.svg +534a0359c2784e152f5c88ce64a56976036d7336c7831775c995977fd3432713 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlecloud.svg +26bfa6c871fba0bbd47d931e408b3e704abdcc1693353cc8f9d1846c2faaa732 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemaps.svg +ae7adb7a21d438caca54f88c2b864975893017eab16f4c0a4746c07b4cf24c74 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemeet.svg +81818efe46f3d59d5d320063df77746016dc2c4a64b94be5abcf26824f74c6e7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleplay.svg +6ddcf298c1664c345a2bb723fe1b73d6294fb949b2f06c4df598a583d96153f7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__grammarly.svg +52044c50a9c97d2995507f0f68c13944597a69d1acf2bf7dea6718b5edfb444c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__icloud.svg +f53af2d1fc5292ba1433b5c1faf50005ce6a997fa302d1816989929f379a59dc *root/www/luci-static/resources/appflow-icons/svg/simple-icons__instagram.svg +3205b3bf8fc00ca140cd96bd976d3c6c993722bdba5e77b1dec77d3e5be629a7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__intuit.svg +5b8108bf84d1ed4ea424157b2d14720cab07734c7119379de7d58567c7495dbe *root/www/luci-static/resources/appflow-icons/svg/simple-icons__itunes.svg +667df35f97be224c0afff6c6f49fd8373c25fc408772137e9eba03f24bde3664 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__kakaotalk.svg +edf5e5c9b8f8940ef3689750ad81359f080f8eb742df2a10a6806f3d5a745d0d *root/www/luci-static/resources/appflow-icons/svg/simple-icons__kaspersky.svg +b62430462354f95914536d7205128647d5563cdb805bc9aca000300aaed816c8 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__line.svg +15e31892dd33b2a53fb1e2c9a003012a8976352f64dfe494e045e7113f534457 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__malwarebytes.svg +b6e25beff2c54363d6c79d58a7c924cd484908c8f6203388b6c1fe34accfffaa *root/www/luci-static/resources/appflow-icons/svg/simple-icons__mastercard.svg +3b846379c62abe0cde7bb19657e036cab59fc8c318c9955ee5717dde5aa85dcd *root/www/luci-static/resources/appflow-icons/svg/simple-icons__mcafee.svg +6791cb013d660aa09ec15a05169759b186ed73a6db89b8d583dcbcfa9aaf20b4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__mlb.svg +84edf371865e90465f5b41ac6023a040ded58c0c925735dc3f8ed1ce0fa2e4a1 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__nba.svg +67dbc495988bd5667a09ed4f56e14db4070a44c7a74e297b146c229f70fd6f6e *root/www/luci-static/resources/appflow-icons/svg/simple-icons__netflix.svg +270ad47c7cfa6d3f915458a76462806ee90699828f9c553df1fa128636db9999 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__nhl.svg +c886ea057184fafe08342bbc18b10006f403ad21d5665a2c9f97f94df941eb30 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__openvpn.svg +47b46a688d2117b938251c0a36568593562a07c221493f9d04bd1e0e536b1456 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__paloaltonetworks.svg +e41b7f4c02a46676971ede09cc827a8909e194b41f2c79f25b048f98dda38cae *root/www/luci-static/resources/appflow-icons/svg/simple-icons__pandora.svg +e8ea6928d3386f81dc6d4c1f58b48e7a29d29bb578b2e3dd808638bd97c63d33 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__paypal.svg +d0736c4b1390f895ed9bc156cc887d3bd65c5b66258c00ad07888e9ec7d729ad *root/www/luci-static/resources/appflow-icons/svg/simple-icons__pinterest.svg +b68b4d7b63443759b9c4d77a5501c5758eeae06a6d594d278d5eb6cb4d3dbbe4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__playstation.svg +d380e190c067ac735a62e69d41fa578d68895c1de51d3fe1a448ed204bb88d51 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__quora.svg +00e5008d56567543cf956561e61d5bf9bf629f520c7488a4d9a00164c29dd8ba *root/www/luci-static/resources/appflow-icons/svg/simple-icons__reddit.svg +3d0e1854a9dad1a8ad11132843f522f948197ec0e0ee49b7a51d5fdf44230568 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__redhat.svg +b80c5880b88b8e489b2da7753bed612b3ce6b2948a3f995ad5d31050f443b520 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__riotgames.svg +9245b2f23fde91a5ef34f36aea32c1bf08a1a1254c08b86e7f060a4f72986234 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__roblox.svg +4a241ca8ebab6e71883b172acb9fb454efa69f308e4f2c31ec80668aeb6627d9 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__samsung.svg +79127bd0cdb41e5940ddbe895da58c4fe1fb6a912a7178f4bb4e108fb39c8758 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__sap.svg +a5ea31f53511bb0b3663cb73ff96eaa8d0451fe9cad586625adfc02b729e4d4d *root/www/luci-static/resources/appflow-icons/svg/simple-icons__shopify.svg +391f46b8feaef281a44dfdf40035d9a4546ded17d2c3eb88d6614762779f1930 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__signal.svg +fbbc31c5544954fbf7525dddbc0d3e79990bb89461f54a94917b6dffc2c5130e *root/www/luci-static/resources/appflow-icons/svg/simple-icons__snapchat.svg +02bac3d2cb8c1091aeae61219803dadbb4364dd636e50f60567d25dfcde7c12c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__soundcloud.svg +b9ec9bec7769c35643981586d6ea0d675aaf3871b32f8a291bcac7a87fff0bc1 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__spotify.svg +5eef8f31106b81956ed908490bcf8c73abe476aa58bb9041acdf70b0d42ebcae *root/www/luci-static/resources/appflow-icons/svg/simple-icons__steam.svg +80ecdde08c2a4cddaa64447fcbfafab81326864671cd33b7fc3d40c4c8661dfc *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tailscale.svg +4bb8c3c6a4d0ce550334f54e7a29ae9ba097123b4174d898c842297d2295d279 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__teamviewer.svg +147fd8f8923e7e5f463fe98c0eb9913bead6b2ae59935728cda141002ec8a7c9 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__telegram.svg +4b94d9eaf23849d6feec7a9c819e3699a35cbb76045ba7d482d17cbcc97414a5 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tesla.svg +7d30363de9f7e880dd32d920736f9187f867d395ae0f6229e232e81a578ae2c6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__threema.svg +6f54ac8d325faacea8935bdc44cbed60206a6b408641799e5fea1cba7c1a0af7 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tiktok.svg +59cd6dc0b427704427a3c64801a56f9acdbd3315dd743321d1a5550a36380037 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tinder.svg +25c6d78e5606fab85d0d3a8424108be7ddd70bee872cb9cff472568107a34bfc *root/www/luci-static/resources/appflow-icons/svg/simple-icons__tumblr.svg +dbe27bc02a01b8d89b6259cacef328c9fa70bb13c29f38c36eb1421863f6f0b6 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__twitch.svg +b0aca674c881a07bb60b26afbcee07093ae419e29ee38e376f938817fa66ae03 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__ubuntu.svg +2cc123deda743aeb31e3a9cba6bdd73bee79475704dfba9cf0c7b46023095914 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__viber.svg +8769e0252009f9babc5f1de2ba6c0638411a2104e7f6f3af6b13cc8310cbd992 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__vimeo.svg +7cce4eea7b16b6eddf60d7d78b7b814d13ab3001bfa5e184480a7ff4a895e4ad *root/www/luci-static/resources/appflow-icons/svg/simple-icons__visa.svg +8fb209a53a61618c3483594b3e070481a35575d6aaecbe00a6fe386670c8fb1c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__whatsapp.svg +2b2acdcf513d7e4d4d6a137bc047d64162ab55955602a01984edf81019efa9f4 *root/www/luci-static/resources/appflow-icons/svg/simple-icons__wikipedia.svg +5038808acbbc4e6edda16cbeb1cc6dec80e4e4ee4e227e039c41229fa222aa8c *root/www/luci-static/resources/appflow-icons/svg/simple-icons__youtube.svg +da5b2f13d38ca61c88a227f58e3bebcbcbb55412d97678ecff49ae7c603f9a7b *root/www/luci-static/resources/appflow-icons/svg/simple-icons__zoom.svg +022aa16a1224b8e5e955eeb02d4ff5f28b4092c547bf8cbcd62f123064b14828 *root/www/luci-static/resources/appflow-icons/manifest.json diff --git a/luci-app-appflow/icons/Makefile b/luci-app-appflow/icons/Makefile new file mode 100644 index 000000000..bedfb429b --- /dev/null +++ b/luci-app-appflow/icons/Makefile @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Copyright 2026 VolanticSystems + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-appflow-icons +PKG_VERSION:=1.0.1 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 AND CC0-1.0 +PKG_LICENSE_FILES:=licenses/LICENSE.simple-icons licenses/LICENSE.dashboard-icons +PKG_MAINTAINER:=Bob + +LUCI_TITLE:=Optional brand icon pack for luci-app-appflow +LUCI_DESCRIPTION:=Adds recognizable per-application icons (Netflix, YouTube, \ + Spotify, etc.) to the luci-app-appflow traffic dashboard. Purely \ + cosmetic and entirely optional: luci-app-appflow renders letter-tile \ + avatars on its own and probes for this package's icon directory at \ + render time, falling back cleanly when it is absent. Installing or \ + removing this package has no effect on core functionality. See \ + docs/DESIGN.md section 9 in the parent luci-app-appflow package for the \ + rationale (legal blast-radius isolation: icons are third-party \ + trademarks regardless of collection license, so this package can be \ + delisted on its own without touching core). +LUCI_DEPENDS:= +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-appflow/icons/README.md b/luci-app-appflow/icons/README.md new file mode 100644 index 000000000..c3fa6ba59 --- /dev/null +++ b/luci-app-appflow/icons/README.md @@ -0,0 +1,122 @@ +# luci-app-appflow-icons + +Optional, separately-installable brand icon pack for +[`luci-app-appflow`](../README.md). See parent `docs/DESIGN.md` section 9 for +the roadmap item and rationale this package implements. + +## What it is + +`luci-app-appflow` identifies per-application traffic using the Netify Agent +(netifyd) and, on its own, renders every detected application as a +letter-tile avatar colored by category. This package adds real icons, +Netflix, YouTube, Spotify, Microsoft, and 97 other recognizable brands, for +the applications netifyd can actually detect, so the dashboard reads more +like a vendor DPI screen and less like a spreadsheet. + +**It is entirely optional.** Installing it changes only how already-detected +traffic is drawn; it adds no detection logic, no new dependency, and no +attack surface beyond static file serving. Removing it drops the dashboard +back to letter-tile avatars, nothing else changes. Core `luci-app-appflow` +has no package dependency on this pack (`LUCI_DEPENDS:=` is deliberately +empty); the coupling is runtime-soft in both directions: + +- core probes for `appflow-icons/manifest.json` at render time and falls + back cleanly when this package is absent; +- this package depends on nothing and does nothing but ship static files, + it breaks nothing if `luci-app-appflow` itself is ever removed or updated + incompatibly. + +That soft coupling is deliberate legal blast-radius isolation: every icon +here is a redrawing of a third party's trademark (see +[`licenses/TRADEMARKS.md`](licenses/TRADEMARKS.md)), so if a rights holder +ever objects, this package is delisted or trimmed on its own, core is +untouched either way. + +## What's in the box + +| | | +|---|---| +| Applications with an icon | 101 (of 416 in the Netify Agent free signature set) | +| Unique SVG files shipped | 95 (a few app tags share one icon, e.g. `WhatsApp` and `WhatsApp/Call` both use the WhatsApp mark) | +| Total SVG payload | 124,590 bytes (~122 KiB) | +| Source: simple-icons (CC0-1.0) | 75 unique files / 78 manifest entries | +| Source: dashboard-icons (Apache-2.0) | 20 unique files / 23 manifest entries | + +simple-icons was preferred whenever both catalogs had a valid match, since +its CC0-1.0 collection license is unambiguous; dashboard-icons filled the +remainder. See [`licenses/LICENSE.simple-icons`](licenses/LICENSE.simple-icons) +and [`licenses/LICENSE.dashboard-icons`](licenses/LICENSE.dashboard-icons) for +the full license text and per-source scope notes, and +[`licenses/TRADEMARKS.md`](licenses/TRADEMARKS.md) for why a permissive +collection license is not the same thing as a trademark clearance. + +## Layout + +``` +icons/ +├── Makefile # LuCI feed style, PKGARCH:=all, no deps +├── README.md # this file +├── CHECKSUMS.sha256 # sha256 of every shipped svg + manifest.json +├── licenses/ +│ ├── LICENSE.simple-icons # CC0-1.0 + source URL + scope note +│ ├── LICENSE.dashboard-icons # Apache-2.0 + source URL + scope note +│ └── TRADEMARKS.md # nominative-use rationale + takedown note +└── root/www/luci-static/resources/appflow-icons/ + ├── manifest.json # {"": {"file": "", "source": "..."}} + └── svg/ # 95 SVG files, source__slug.svg naming +``` + +`root/` mirrors the LuCI feed convention used by the parent package +(`include $(TOPDIR)/feeds/luci/luci.mk` copies its contents verbatim onto +the target filesystem), landing everything under +`/www/luci-static/resources/appflow-icons/` on the router, a normal LuCI +static-resource path, servable and cacheable exactly like any other +`luci-static` asset, no server-side code involved. + +## manifest.json contract + +`manifest.json` is keyed by the exact application **tag string** netifyd's +`definitions` event reports (the same string `luci-app-appflow`'s frontend +already receives at runtime, e.g. `"netify.netflix"` for Netify's own +namespaced app ids, or a plain tag like `"Spotify"` for nDPI protocol-space +ids, see parent `docs/DESIGN.md` section 2.3 for the two id spaces). Each +value names the icon file relative to `svg/` and which collection it came +from: + +```json +{ + "netify.netflix": { "file": "simple-icons__netflix.svg", "source": "simple-icons" }, + "Skype/Teams/Call": { "file": "dashboard-icons__skype.svg", "source": "dashboard-icons" } +} +``` + +A consumer only needs to look up the detected tag in this object; a miss +means "no icon shipped for this app," which is the expected, common case +(315 of 416 detectable applications have none) and should fall back to a +letter-tile avatar exactly as when this package isn't installed at all. + +## Regenerating / extending this pack + +The id-to-icon mapping was curated offline against a `definitions` event +captured from Netify Agent 4.4.7 (416 applications), matched by exact/ +normalized name against the simple-icons and dashboard-icons slug catalogs, +with no fuzzy or guessed matches, anything ambiguous (15 applications) was +left out rather than auto-picked, and only the ~100 most household- +recognizable matches ("tier1") were selected for actual SVG download and +inclusion in this package; the remainder of the matched set ("tier2", 73 +applications) was identified but not bundled. `manifest.json` in this +package is the tier1-only, validated output of that pass: every entry here +was checked at build time to (a) reference an SVG file that exists on disk +and parses as well-formed XML with an `` root, and (b) use a tag that +is actually present in the current `detectable-apps` extraction, anything +failing either check is dropped rather than shipped. To extend coverage +(add tier2 apps, or re-run against a newer netifyd's signature set), repeat +that curation pass and re-run the same validation before regenerating +`manifest.json` and `CHECKSUMS.sha256`. + +## Verifying the shipped files + +```sh +cd icons +sha256sum -c CHECKSUMS.sha256 +``` diff --git a/luci-app-appflow/icons/licenses/LICENSE.dashboard-icons b/luci-app-appflow/icons/licenses/LICENSE.dashboard-icons new file mode 100644 index 000000000..cde314d18 --- /dev/null +++ b/luci-app-appflow/icons/licenses/LICENSE.dashboard-icons @@ -0,0 +1,224 @@ +dashboard-icons — icon collection license +=========================================== + +Applies to every SVG in this package whose manifest.json "source" field is +"dashboard-icons" (20 of the 95 unique SVG files shipped; 23 of the 101 +manifest entries reference one of these files, a few app tags sharing one +icon -- see README.md). + +Source project: https://github.com/homarr-labs/dashboard-icons +Source license file: https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/LICENSE +Fetched: 2026-08-21 + +Scope note: this Apache-2.0 license covers the dashboard-icons project's own +code and aggregation work. It is not a copyright grant from each depicted +brand over its own logo artwork -- the project's own README states the icons +are used "for identification purposes only and do not imply endorsement," +i.e. the same nominative-fair-use basis this package relies on (see +TRADEMARKS.md in this directory). Treated here as less license-clean than +the simple-icons (CC0) entries for that reason; simple-icons was preferred +wherever both catalogs had a valid match. + +------------------------------------------------------------------------ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) 2024 Bjorn Lammers, Meier Lukas, Thomas Camlong and Homarr Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/luci-app-appflow/icons/licenses/LICENSE.simple-icons b/luci-app-appflow/icons/licenses/LICENSE.simple-icons new file mode 100644 index 000000000..072da982d --- /dev/null +++ b/luci-app-appflow/icons/licenses/LICENSE.simple-icons @@ -0,0 +1,49 @@ +simple-icons — icon collection license +======================================= + +Applies to every SVG in this package whose manifest.json "source" field is +"simple-icons" (75 of the 95 unique SVG files shipped; 78 of the 101 +manifest entries reference one of these files, a few app tags sharing one +icon -- see README.md). + +Source project: https://github.com/simple-icons/simple-icons +Source license file: https://raw.githubusercontent.com/simple-icons/simple-icons/develop/LICENSE.md +Fetched: 2026-08-21 + +This license covers the icon artwork/markup as published by the simple-icons +project. It does NOT waive trademark rights in the brand names or logos the +artwork depicts (see clause 4.1 below) — see TRADEMARKS.md in this directory +for how this package relies on nominative fair use instead. + +------------------------------------------------------------------------ + +# CC0 1.0 Universal + +## Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: + 1. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; + 2. moral rights retained by the original author(s) and/or performer(s); + 3. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; + 4. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(i), below; + 5. rights protecting the extraction, dissemination, use and reuse of data in a Work; + 6. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and + 7. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. + +4. Limitations and Disclaimers. + 1. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. + 2. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. + 3. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. + 4. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. + +For more information, please see . diff --git a/luci-app-appflow/icons/licenses/TRADEMARKS.md b/luci-app-appflow/icons/licenses/TRADEMARKS.md new file mode 100644 index 000000000..1beb63198 --- /dev/null +++ b/luci-app-appflow/icons/licenses/TRADEMARKS.md @@ -0,0 +1,63 @@ +# Trademark notice + +The icons shipped under `icons/root/www/luci-static/resources/appflow-icons/svg/` +depict the brand marks of third-party companies and products (Netflix, +Spotify, Microsoft, and others) that `luci-app-appflow` may detect on your +network traffic. Those marks are the property of their respective owners. + +## What the collection licenses do and don't cover + +`LICENSE.simple-icons` (CC0-1.0) and `LICENSE.dashboard-icons` (Apache-2.0) in +this directory license the icon *artwork/markup* as published by those +collection projects. Neither license waives, licenses, or otherwise affects +trademark rights in the marks the artwork depicts: + +- CC0-1.0 says so explicitly, clause 4.1: "No trademark or patent rights held + by Affirmer are waived, abandoned, surrendered, licensed or otherwise + affected by this document." +- Apache-2.0 says so explicitly, clause 6: it "does not grant permission to + use the trade names, trademarks, service marks, or product names of the + Licensor," and separately grants no rights at all in a third party's marks + in the first place — dashboard-icons' own project documentation describes + its icons as used "for identification purposes only," not as a rights + grant over each depicted brand's logo. + +## Basis for using them here + +This package uses each mark strictly to *label detected network traffic* for +the application it identifies — showing a Netflix icon next to a flow +netifyd has classified as `netify.netflix`, and nothing more. That is a +textbook case of **nominative fair use**: referring to a company's own +product using its own mark, in a context (interoperability / identification) +where no reasonable viewer would infer sponsorship or endorsement. This +package: + +- ships upstream artwork unmodified (no recoloring or redrawing performed + here), +- uses each icon only to identify the corresponding detected application, +- does not imply that Netify, any OpenWrt project, or the maintainer of + `luci-app-appflow` is endorsed by, or affiliated with, any depicted brand. + +This is the same basis dashboard-style projects such as Homarr and Heimdall +operate on. It is a reasonable, common-practice position — not a substitute +for legal review, and not a representation that every jurisdiction treats +nominative use identically. + +## Why this is a separate package (see parent docs/DESIGN.md section 9) + +`luci-app-appflow-icons` is deliberately split from `luci-app-appflow` core +for legal blast-radius isolation. Brand icons remain third-party trademarks +regardless of the collection license they were sourced under; core's +dependency on this package is entirely soft (it probes for the icon +directory at render time and falls back to letter-tile avatars when absent), +so this package can be removed without affecting core functionality in any +way. + +## Takedown + +If you are a rights holder and object to your mark's inclusion here, the +remedy is to ask for this package to be delisted or to have the specific +icon removed. Because the coupling to core is soft in both directions, +removing this package (or an individual icon + its `manifest.json` entry +from it) has no effect on `luci-app-appflow`'s core functionality — traffic +for the affected application(s) simply reverts to a letter-tile avatar. diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/manifest.json b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/manifest.json new file mode 100644 index 000000000..ded47dd5f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/manifest.json @@ -0,0 +1,406 @@ +{ + "Skype/Teams/Call": { + "file": "dashboard-icons__skype.svg", + "source": "dashboard-icons" + }, + "SignalCall": { + "file": "simple-icons__signal.svg", + "source": "simple-icons" + }, + "Xbox": { + "file": "dashboard-icons__xbox.svg", + "source": "dashboard-icons" + }, + "netify.yahoo": { + "file": "dashboard-icons__yahoo.svg", + "source": "dashboard-icons" + }, + "Steam": { + "file": "simple-icons__steam.svg", + "source": "simple-icons" + }, + "netify.facebook": { + "file": "simple-icons__facebook.svg", + "source": "simple-icons" + }, + "netify.twitter": { + "file": "dashboard-icons__twitter.svg", + "source": "dashboard-icons" + }, + "netify.dropbox": { + "file": "simple-icons__dropbox.svg", + "source": "simple-icons" + }, + "netify.google-maps": { + "file": "simple-icons__googlemaps.svg", + "source": "simple-icons" + }, + "netify.youtube": { + "file": "simple-icons__youtube.svg", + "source": "simple-icons" + }, + "netify.netflix": { + "file": "simple-icons__netflix.svg", + "source": "simple-icons" + }, + "WhatsApp": { + "file": "simple-icons__whatsapp.svg", + "source": "simple-icons" + }, + "netify.apple-icloud": { + "file": "simple-icons__icloud.svg", + "source": "simple-icons" + }, + "Viber": { + "file": "simple-icons__viber.svg", + "source": "simple-icons" + }, + "netify.apple-itunes": { + "file": "simple-icons__itunes.svg", + "source": "simple-icons" + }, + "TeamViewer": { + "file": "simple-icons__teamviewer.svg", + "source": "simple-icons" + }, + "SAP": { + "file": "simple-icons__sap.svg", + "source": "simple-icons" + }, + "netify.spotify": { + "file": "simple-icons__spotify.svg", + "source": "simple-icons" + }, + "OpenVPN": { + "file": "simple-icons__openvpn.svg", + "source": "simple-icons" + }, + "Oracle": { + "file": "dashboard-icons__oracle.svg", + "source": "dashboard-icons" + }, + "netify.wikipedia": { + "file": "simple-icons__wikipedia.svg", + "source": "simple-icons" + }, + "netify.amazon": { + "file": "dashboard-icons__amazon.svg", + "source": "dashboard-icons" + }, + "netify.ebay": { + "file": "simple-icons__ebay.svg", + "source": "simple-icons" + }, + "Telegram": { + "file": "simple-icons__telegram.svg", + "source": "simple-icons" + }, + "netify.pandora": { + "file": "simple-icons__pandora.svg", + "source": "simple-icons" + }, + "KakaoTalk/Voice": { + "file": "simple-icons__kakaotalk.svg", + "source": "simple-icons" + }, + "netify.twitch": { + "file": "simple-icons__twitch.svg", + "source": "simple-icons" + }, + "netify.snapchat": { + "file": "simple-icons__snapchat.svg", + "source": "simple-icons" + }, + "netify.instagram": { + "file": "simple-icons__instagram.svg", + "source": "simple-icons" + }, + "netify.microsoft": { + "file": "dashboard-icons__microsoft.svg", + "source": "dashboard-icons" + }, + "netify.cloudflare": { + "file": "simple-icons__cloudflare.svg", + "source": "simple-icons" + }, + "netify.microsoft-onedrive": { + "file": "dashboard-icons__microsoft-onedrive.svg", + "source": "dashboard-icons" + }, + "netify.weibo": { + "file": "dashboard-icons__weibo.svg", + "source": "dashboard-icons" + }, + "Google/Meet/Duo": { + "file": "simple-icons__googlemeet.svg", + "source": "simple-icons" + }, + "WhatsApp/Call": { + "file": "simple-icons__whatsapp.svg", + "source": "simple-icons" + }, + "Skype/Teams": { + "file": "dashboard-icons__skype.svg", + "source": "dashboard-icons" + }, + "ZOOM": { + "file": "simple-icons__zoom.svg", + "source": "simple-icons" + }, + "Snapchat/Call": { + "file": "simple-icons__snapchat.svg", + "source": "simple-icons" + }, + "Riot/Games": { + "file": "simple-icons__riotgames.svg", + "source": "simple-icons" + }, + "Threema": { + "file": "simple-icons__threema.svg", + "source": "simple-icons" + }, + "AVAST": { + "file": "simple-icons__avast.svg", + "source": "simple-icons" + }, + "Line/Call": { + "file": "simple-icons__line.svg", + "source": "simple-icons" + }, + "Tailscale": { + "file": "simple-icons__tailscale.svg", + "source": "simple-icons" + }, + "netify.github": { + "file": "simple-icons__github.svg", + "source": "simple-icons" + }, + "netify.reddit": { + "file": "simple-icons__reddit.svg", + "source": "simple-icons" + }, + "netify.playstation": { + "file": "simple-icons__playstation.svg", + "source": "simple-icons" + }, + "netify.bing": { + "file": "dashboard-icons__bing.svg", + "source": "dashboard-icons" + }, + "netify.mlb": { + "file": "simple-icons__mlb.svg", + "source": "simple-icons" + }, + "netify.nhl": { + "file": "simple-icons__nhl.svg", + "source": "simple-icons" + }, + "netify.shopify": { + "file": "simple-icons__shopify.svg", + "source": "simple-icons" + }, + "netify.roblox": { + "file": "simple-icons__roblox.svg", + "source": "simple-icons" + }, + "netify.google-ads": { + "file": "simple-icons__googleads.svg", + "source": "simple-icons" + }, + "netify.vimeo": { + "file": "simple-icons__vimeo.svg", + "source": "simple-icons" + }, + "netify.soundcloud": { + "file": "simple-icons__soundcloud.svg", + "source": "simple-icons" + }, + "netify.airbnb": { + "file": "simple-icons__airbnb.svg", + "source": "simple-icons" + }, + "netify.paypal": { + "file": "simple-icons__paypal.svg", + "source": "simple-icons" + }, + "netify.tumblr": { + "file": "simple-icons__tumblr.svg", + "source": "simple-icons" + }, + "netify.linkedin": { + "file": "dashboard-icons__linkedin.svg", + "source": "dashboard-icons" + }, + "netify.pinterest": { + "file": "simple-icons__pinterest.svg", + "source": "simple-icons" + }, + "netify.firefox": { + "file": "simple-icons__firefox.svg", + "source": "simple-icons" + }, + "netify.amazon-aws": { + "file": "dashboard-icons__amazon-web-services.svg", + "source": "dashboard-icons" + }, + "netify.baidu": { + "file": "simple-icons__baidu.svg", + "source": "simple-icons" + }, + "netify.adobe": { + "file": "dashboard-icons__adobe.svg", + "source": "dashboard-icons" + }, + "netify.salesforce": { + "file": "dashboard-icons__salesforce.svg", + "source": "dashboard-icons" + }, + "netify.avast": { + "file": "simple-icons__avast.svg", + "source": "simple-icons" + }, + "netify.malwarebytes": { + "file": "simple-icons__malwarebytes.svg", + "source": "simple-icons" + }, + "netify.mcafee": { + "file": "simple-icons__mcafee.svg", + "source": "simple-icons" + }, + "netify.kaspersky": { + "file": "simple-icons__kaspersky.svg", + "source": "simple-icons" + }, + "netify.bitdefender": { + "file": "simple-icons__bitdefender.svg", + "source": "simple-icons" + }, + "netify.sophos": { + "file": "dashboard-icons__sophos.svg", + "source": "dashboard-icons" + }, + "netify.epic-games": { + "file": "simple-icons__epicgames.svg", + "source": "simple-icons" + }, + "netify.docker": { + "file": "simple-icons__docker.svg", + "source": "simple-icons" + }, + "netify.gitlab": { + "file": "simple-icons__gitlab.svg", + "source": "simple-icons" + }, + "netify.slack": { + "file": "dashboard-icons__slack.svg", + "source": "dashboard-icons" + }, + "netify.xbox": { + "file": "dashboard-icons__xbox.svg", + "source": "dashboard-icons" + }, + "netify.ubuntu": { + "file": "simple-icons__ubuntu.svg", + "source": "simple-icons" + }, + "netify.discord": { + "file": "simple-icons__discord.svg", + "source": "simple-icons" + }, + "netify.fitbit": { + "file": "simple-icons__fitbit.svg", + "source": "simple-icons" + }, + "netify.tiktok": { + "file": "simple-icons__tiktok.svg", + "source": "simple-icons" + }, + "netify.intuit": { + "file": "simple-icons__intuit.svg", + "source": "simple-icons" + }, + "netify.grammarly": { + "file": "simple-icons__grammarly.svg", + "source": "simple-icons" + }, + "netify.hulu": { + "file": "dashboard-icons__hulu.svg", + "source": "dashboard-icons" + }, + "netify.tinder": { + "file": "simple-icons__tinder.svg", + "source": "simple-icons" + }, + "netify.disney-plus": { + "file": "dashboard-icons__disney-plus.svg", + "source": "dashboard-icons" + }, + "netify.xbox-live": { + "file": "dashboard-icons__xbox.svg", + "source": "dashboard-icons" + }, + "netify.dazn": { + "file": "simple-icons__dazn.svg", + "source": "simple-icons" + }, + "netify.palo-alto-networks": { + "file": "simple-icons__paloaltonetworks.svg", + "source": "simple-icons" + }, + "netify.tesla": { + "file": "simple-icons__tesla.svg", + "source": "simple-icons" + }, + "netify.nba": { + "file": "simple-icons__nba.svg", + "source": "simple-icons" + }, + "netify.red-hat": { + "file": "simple-icons__redhat.svg", + "source": "simple-icons" + }, + "netify.google-cloud": { + "file": "simple-icons__googlecloud.svg", + "source": "simple-icons" + }, + "netify.alipay": { + "file": "simple-icons__alipay.svg", + "source": "simple-icons" + }, + "netify.visa": { + "file": "simple-icons__visa.svg", + "source": "simple-icons" + }, + "netify.samsung-tv": { + "file": "simple-icons__samsung.svg", + "source": "simple-icons" + }, + "netify.coinbase": { + "file": "simple-icons__coinbase.svg", + "source": "simple-icons" + }, + "netify.mastercard": { + "file": "simple-icons__mastercard.svg", + "source": "simple-icons" + }, + "netify.fifa": { + "file": "simple-icons__fifa.svg", + "source": "simple-icons" + }, + "netify.quora": { + "file": "simple-icons__quora.svg", + "source": "simple-icons" + }, + "netify.google-play": { + "file": "simple-icons__googleplay.svg", + "source": "simple-icons" + }, + "netify.crowdstrike": { + "file": "dashboard-icons__crowdstrike.svg", + "source": "dashboard-icons" + }, + "netify.windows": { + "file": "dashboard-icons__microsoft-windows.svg", + "source": "dashboard-icons" + } +} diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__adobe.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__adobe.svg new file mode 100644 index 000000000..8931c4979 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__adobe.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon-web-services.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon-web-services.svg new file mode 100644 index 000000000..7c989b0f6 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon-web-services.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon.svg new file mode 100644 index 000000000..97afc4ab6 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__amazon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__bing.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__bing.svg new file mode 100644 index 000000000..6b6e98903 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__bing.svg @@ -0,0 +1 @@ + diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__crowdstrike.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__crowdstrike.svg new file mode 100644 index 000000000..b48de3f12 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__crowdstrike.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__disney-plus.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__disney-plus.svg new file mode 100644 index 000000000..f4111e13c --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__disney-plus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__hulu.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__hulu.svg new file mode 100644 index 000000000..e85f3bce5 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__hulu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__linkedin.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__linkedin.svg new file mode 100644 index 000000000..3bc0d474f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__linkedin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-onedrive.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-onedrive.svg new file mode 100644 index 000000000..499a4ade6 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-onedrive.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-windows.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-windows.svg new file mode 100644 index 000000000..9d1adb5e5 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft-windows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft.svg new file mode 100644 index 000000000..5a10e110b --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__microsoft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__oracle.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__oracle.svg new file mode 100644 index 000000000..04c8d4136 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__oracle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__salesforce.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__salesforce.svg new file mode 100644 index 000000000..527a0cd6b --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__salesforce.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__skype.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__skype.svg new file mode 100644 index 000000000..710d8c8f6 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__skype.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__slack.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__slack.svg new file mode 100644 index 000000000..93234ea6f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__sophos.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__sophos.svg new file mode 100644 index 000000000..8269d9aab --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__sophos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__twitter.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__twitter.svg new file mode 100644 index 000000000..7d68f90c4 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__twitter.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__weibo.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__weibo.svg new file mode 100644 index 000000000..340ea4765 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__weibo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__xbox.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__xbox.svg new file mode 100644 index 000000000..9141f41ea --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__xbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__yahoo.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__yahoo.svg new file mode 100644 index 000000000..5e4b44874 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/dashboard-icons__yahoo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__airbnb.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__airbnb.svg new file mode 100644 index 000000000..4677c707f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__airbnb.svg @@ -0,0 +1 @@ +Airbnb \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__alipay.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__alipay.svg new file mode 100644 index 000000000..30fa27f22 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__alipay.svg @@ -0,0 +1 @@ +Alipay \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__avast.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__avast.svg new file mode 100644 index 000000000..e5f8e0666 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__avast.svg @@ -0,0 +1 @@ +Avast \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__baidu.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__baidu.svg new file mode 100644 index 000000000..d3e659695 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__baidu.svg @@ -0,0 +1 @@ +Baidu \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__bitdefender.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__bitdefender.svg new file mode 100644 index 000000000..bfae48cb1 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__bitdefender.svg @@ -0,0 +1 @@ +Bitdefender \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__cloudflare.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__cloudflare.svg new file mode 100644 index 000000000..a1cf2d6d3 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__cloudflare.svg @@ -0,0 +1 @@ +Cloudflare \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__coinbase.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__coinbase.svg new file mode 100644 index 000000000..c3ed947a5 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__coinbase.svg @@ -0,0 +1 @@ +Coinbase \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__dazn.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__dazn.svg new file mode 100644 index 000000000..7214af55b --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__dazn.svg @@ -0,0 +1 @@ +DAZN \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__discord.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__discord.svg new file mode 100644 index 000000000..9d7796b8a --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__discord.svg @@ -0,0 +1 @@ +Discord \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__docker.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__docker.svg new file mode 100644 index 000000000..0021a8a7b --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__docker.svg @@ -0,0 +1 @@ +Docker \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__dropbox.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__dropbox.svg new file mode 100644 index 000000000..8faa25173 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__dropbox.svg @@ -0,0 +1 @@ +Dropbox \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__ebay.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__ebay.svg new file mode 100644 index 000000000..74d88a846 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__ebay.svg @@ -0,0 +1 @@ +eBay \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__epicgames.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__epicgames.svg new file mode 100644 index 000000000..d66f87003 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__epicgames.svg @@ -0,0 +1 @@ +Epic Games \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__facebook.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__facebook.svg new file mode 100644 index 000000000..f66767e46 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__facebook.svg @@ -0,0 +1 @@ +Facebook \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__fifa.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__fifa.svg new file mode 100644 index 000000000..49ab1a013 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__fifa.svg @@ -0,0 +1 @@ +FIFA \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__firefox.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__firefox.svg new file mode 100644 index 000000000..e7c9fff23 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__firefox.svg @@ -0,0 +1 @@ +Firefox \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__fitbit.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__fitbit.svg new file mode 100644 index 000000000..3e5dd92fa --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__fitbit.svg @@ -0,0 +1 @@ +Fitbit \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__github.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__github.svg new file mode 100644 index 000000000..538ec5bf2 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__github.svg @@ -0,0 +1 @@ +GitHub \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__gitlab.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__gitlab.svg new file mode 100644 index 000000000..e4a29630f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__gitlab.svg @@ -0,0 +1 @@ +GitLab \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleads.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleads.svg new file mode 100644 index 000000000..fc695ad09 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleads.svg @@ -0,0 +1 @@ +Google Ads \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlecloud.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlecloud.svg new file mode 100644 index 000000000..b7b4dcc06 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlecloud.svg @@ -0,0 +1 @@ +Google Cloud \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemaps.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemaps.svg new file mode 100644 index 000000000..2033bd85f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemaps.svg @@ -0,0 +1 @@ +Google Maps \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemeet.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemeet.svg new file mode 100644 index 000000000..344f1d003 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googlemeet.svg @@ -0,0 +1 @@ +Google Meet \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleplay.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleplay.svg new file mode 100644 index 000000000..3ce505fc2 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__googleplay.svg @@ -0,0 +1 @@ +Google Play \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__grammarly.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__grammarly.svg new file mode 100644 index 000000000..da60aef70 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__grammarly.svg @@ -0,0 +1 @@ +Grammarly \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__icloud.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__icloud.svg new file mode 100644 index 000000000..e7c7e0757 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__icloud.svg @@ -0,0 +1 @@ +iCloud \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__instagram.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__instagram.svg new file mode 100644 index 000000000..c0e86b0bf --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__instagram.svg @@ -0,0 +1 @@ +Instagram \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__intuit.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__intuit.svg new file mode 100644 index 000000000..1a4530a9c --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__intuit.svg @@ -0,0 +1 @@ +Intuit \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__itunes.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__itunes.svg new file mode 100644 index 000000000..01e120e44 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__itunes.svg @@ -0,0 +1 @@ +iTunes \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__kakaotalk.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__kakaotalk.svg new file mode 100644 index 000000000..8e9f9b123 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__kakaotalk.svg @@ -0,0 +1 @@ +KakaoTalk \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__kaspersky.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__kaspersky.svg new file mode 100644 index 000000000..1863ca249 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__kaspersky.svg @@ -0,0 +1 @@ +Kaspersky \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__line.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__line.svg new file mode 100644 index 000000000..9e4dc98c2 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__line.svg @@ -0,0 +1 @@ +LINE \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__malwarebytes.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__malwarebytes.svg new file mode 100644 index 000000000..6c641af0b --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__malwarebytes.svg @@ -0,0 +1 @@ +Malwarebytes \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mastercard.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mastercard.svg new file mode 100644 index 000000000..7fffc5389 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mastercard.svg @@ -0,0 +1 @@ +MasterCard \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mcafee.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mcafee.svg new file mode 100644 index 000000000..f10cab7df --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mcafee.svg @@ -0,0 +1 @@ +McAfee \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mlb.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mlb.svg new file mode 100644 index 000000000..01bf2764f --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__mlb.svg @@ -0,0 +1 @@ +MLB \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__nba.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__nba.svg new file mode 100644 index 000000000..64aa09553 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__nba.svg @@ -0,0 +1 @@ +NBA \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__netflix.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__netflix.svg new file mode 100644 index 000000000..8b894edae --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__netflix.svg @@ -0,0 +1 @@ +Netflix \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__nhl.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__nhl.svg new file mode 100644 index 000000000..7703bde4e --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__nhl.svg @@ -0,0 +1 @@ +NHL \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__openvpn.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__openvpn.svg new file mode 100644 index 000000000..198fea5d1 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__openvpn.svg @@ -0,0 +1 @@ +OpenVPN \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__paloaltonetworks.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__paloaltonetworks.svg new file mode 100644 index 000000000..303f6de9b --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__paloaltonetworks.svg @@ -0,0 +1 @@ +Palo Alto Networks \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__pandora.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__pandora.svg new file mode 100644 index 000000000..41b2413a7 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__pandora.svg @@ -0,0 +1 @@ +Pandora \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__paypal.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__paypal.svg new file mode 100644 index 000000000..53e784ed1 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__paypal.svg @@ -0,0 +1 @@ +PayPal \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__pinterest.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__pinterest.svg new file mode 100644 index 000000000..2ce115c6d --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__pinterest.svg @@ -0,0 +1 @@ +Pinterest \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__playstation.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__playstation.svg new file mode 100644 index 000000000..be3370316 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__playstation.svg @@ -0,0 +1 @@ +PlayStation \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__quora.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__quora.svg new file mode 100644 index 000000000..7d2888201 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__quora.svg @@ -0,0 +1 @@ +Quora \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__reddit.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__reddit.svg new file mode 100644 index 000000000..ea0388317 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__reddit.svg @@ -0,0 +1 @@ +Reddit \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__redhat.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__redhat.svg new file mode 100644 index 000000000..e9a23ca71 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__redhat.svg @@ -0,0 +1 @@ +Red Hat \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__riotgames.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__riotgames.svg new file mode 100644 index 000000000..9cb26ead7 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__riotgames.svg @@ -0,0 +1 @@ +Riot Games \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__roblox.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__roblox.svg new file mode 100644 index 000000000..128fb2d54 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__roblox.svg @@ -0,0 +1 @@ +Roblox \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__samsung.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__samsung.svg new file mode 100644 index 000000000..627cbf2c1 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__samsung.svg @@ -0,0 +1 @@ +Samsung \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__sap.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__sap.svg new file mode 100644 index 000000000..9b032ccc6 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__sap.svg @@ -0,0 +1 @@ +SAP \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__shopify.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__shopify.svg new file mode 100644 index 000000000..9a7d37777 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__shopify.svg @@ -0,0 +1 @@ +Shopify \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__signal.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__signal.svg new file mode 100644 index 000000000..c8ee507f2 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__signal.svg @@ -0,0 +1 @@ +Signal \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__snapchat.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__snapchat.svg new file mode 100644 index 000000000..3110fbada --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__snapchat.svg @@ -0,0 +1 @@ +Snapchat \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__soundcloud.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__soundcloud.svg new file mode 100644 index 000000000..d964cc3df --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__soundcloud.svg @@ -0,0 +1 @@ +SoundCloud \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__spotify.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__spotify.svg new file mode 100644 index 000000000..8d4d095f3 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__spotify.svg @@ -0,0 +1 @@ +Spotify \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__steam.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__steam.svg new file mode 100644 index 000000000..affefa518 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__steam.svg @@ -0,0 +1 @@ +Steam \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tailscale.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tailscale.svg new file mode 100644 index 000000000..9a05aa6bd --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tailscale.svg @@ -0,0 +1 @@ +Tailscale \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__teamviewer.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__teamviewer.svg new file mode 100644 index 000000000..d9bd8d227 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__teamviewer.svg @@ -0,0 +1 @@ +TeamViewer \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__telegram.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__telegram.svg new file mode 100644 index 000000000..c46b0ed70 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__telegram.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tesla.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tesla.svg new file mode 100644 index 000000000..77e66012d --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tesla.svg @@ -0,0 +1 @@ +Tesla \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__threema.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__threema.svg new file mode 100644 index 000000000..f39652ca8 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__threema.svg @@ -0,0 +1 @@ +Threema \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tiktok.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tiktok.svg new file mode 100644 index 000000000..57ce3ae5d --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tiktok.svg @@ -0,0 +1 @@ +TikTok \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tinder.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tinder.svg new file mode 100644 index 000000000..193bdf099 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tinder.svg @@ -0,0 +1 @@ +Tinder \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tumblr.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tumblr.svg new file mode 100644 index 000000000..c6547f8a9 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__tumblr.svg @@ -0,0 +1 @@ +Tumblr \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__twitch.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__twitch.svg new file mode 100644 index 000000000..8aaa4a9ad --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__twitch.svg @@ -0,0 +1 @@ +Twitch \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__ubuntu.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__ubuntu.svg new file mode 100644 index 000000000..227f86aa3 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__ubuntu.svg @@ -0,0 +1 @@ +Ubuntu \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__viber.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__viber.svg new file mode 100644 index 000000000..dfb1eedcb --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__viber.svg @@ -0,0 +1 @@ +Viber \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__vimeo.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__vimeo.svg new file mode 100644 index 000000000..cecbfccce --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__vimeo.svg @@ -0,0 +1 @@ +Vimeo \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__visa.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__visa.svg new file mode 100644 index 000000000..6a6527069 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__visa.svg @@ -0,0 +1 @@ +Visa \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__whatsapp.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__whatsapp.svg new file mode 100644 index 000000000..2a6c69fdf --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__whatsapp.svg @@ -0,0 +1 @@ +WhatsApp \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__wikipedia.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__wikipedia.svg new file mode 100644 index 000000000..bc270c195 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__wikipedia.svg @@ -0,0 +1 @@ +Wikipedia \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__youtube.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__youtube.svg new file mode 100644 index 000000000..0492366a2 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__youtube.svg @@ -0,0 +1 @@ +YouTube \ No newline at end of file diff --git a/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__zoom.svg b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__zoom.svg new file mode 100644 index 000000000..a19764320 --- /dev/null +++ b/luci-app-appflow/icons/root/www/luci-static/resources/appflow-icons/svg/simple-icons__zoom.svg @@ -0,0 +1 @@ +Zoom \ No newline at end of file diff --git a/luci-app-appflow/po/templates/appflow.pot b/luci-app-appflow/po/templates/appflow.pot new file mode 100644 index 000000000..7c1751193 --- /dev/null +++ b/luci-app-appflow/po/templates/appflow.pot @@ -0,0 +1,671 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:287 +msgid "%d applications" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1419 +msgid "%d apps, %d devices" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:667 +msgid "%d d ago" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:585 +msgid "%d flows" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:665 +msgid "%d h ago" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:662 +msgid "%d min ago" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:153 +msgid "%d minute chart, %d second interval" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:286 +msgid "%d of %d applications" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:659 +msgid "%d s ago" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:284 +msgid "%d s rate window" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:335 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:473 +msgid "-%d min" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:585 +msgid "1 flow" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:161 +msgid "12 buckets of 5 min" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:194 +msgid "AI assistants" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:195 +msgid "AI developer" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:196 +msgid "AI infrastructure" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:197 +msgid "AI media" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:144 +msgid "Adult" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:145 +msgid "Advertiser" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:312 +msgid "All traffic" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:109 +msgid "App traffic statistics" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:219 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:129 +#: applications/luci-app-appflow/root/usr/share/luci/menu.d/luci-app-appflow.json:3 +msgid "AppFlow" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1371 +msgid "AppFlow also needs the Netify Agent running (%s), since that is where the per-application classification comes from." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:563 +msgid "AppFlow statistics cleared." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1361 +msgid "AppFlow: access denied" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:516 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:27 +msgid "Application" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:180 +msgid "Authentication" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:146 +msgid "Business" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:147 +msgid "CDN" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:550 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:190 +msgid "Categories" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:170 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:201 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:89 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:554 +msgid "Clear" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:542 +msgid "Clear statistics" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:482 +msgid "Client" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:524 +msgid "Close" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:383 +msgid "Collecting data…" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:321 +msgid "Collecting samples…" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1413 +msgid "Collector state unknown" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1423 +msgid "Collector uptime: %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:569 +msgid "Could not clear the statistics: %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:657 +msgid "Current rate" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:148 +msgid "Cybersecurity" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:460 +msgid "Data usage" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:181 +msgid "Database" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:536 +msgid "Details" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:655 +msgid "Device" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:386 +msgid "Device: %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:446 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:478 +msgid "Devices" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1324 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:313 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:518 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:28 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:443 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:484 +msgid "Download" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:926 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1146 +msgid "Download: %s, upload: %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:315 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:518 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:658 +msgid "Downloaded" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:150 +msgid "Education" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:151 +msgid "Entertainment" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:182 +msgid "File Server" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:152 +msgid "File Sharing" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:115 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:116 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:78 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:79 +msgid "Filter by application or category" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:102 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:103 +msgid "Filter by device name, MAC or IP" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:153 +msgid "Financial" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1416 +msgid "Flows tracked: %d" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:154 +msgid "Gambling" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:155 +msgid "Games" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:156 +msgid "Government" +msgstr "" + +#: applications/luci-app-appflow/root/usr/share/rpcd/acl.d/luci-app-appflow.json:3 +msgid "Grant access to per-application traffic statistics. The 'apps' method is granted because the overview drills into a device to show what it is doing; it returns an aggregate per device and application from the live flow table. 'flows' remains ungranted: that is the per-connection table, with remote addresses and ports." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:157 +msgid "Hosting" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:509 +msgid "Hosts" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:183 +msgid "Infrastructure" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:149 +msgid "IoT Devices" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:483 +msgid "Last seen" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:221 +msgid "Live per-application traffic, classified on the router by the Netify Agent. Nothing on this page leaves the device." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:151 +msgid "Live throughput" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:158 +msgid "Mail" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:159 +msgid "Malware" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:184 +msgid "Media" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:185 +msgid "Media Provider" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:160 +msgid "Messaging" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:212 +msgid "Multicast / Broadcast" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1411 +msgid "Netify agent connected" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1412 +msgid "Netify agent disconnected" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:186 +msgid "Networking" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:161 +msgid "News" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:645 +msgid "No active devices." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:324 +msgid "No application matches \"%s\"." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:487 +msgid "No application matches that filter." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:488 +msgid "No application traffic in the current window." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:218 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:325 +msgid "No application traffic recorded in the last hour." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:562 +msgid "No categories yet." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1315 +msgid "No data" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:644 +msgid "No device matches that filter." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:505 +msgid "No per-device breakdown available for this application." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:476 +msgid "No time series available for this application." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:162 +msgid "OS & Software Updates" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:580 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:592 +msgid "Other" +msgstr "" + +#: applications/luci-app-appflow/root/usr/share/luci/menu.d/luci-app-appflow.json:16 +msgid "Overview" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:99 +msgid "Past day" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:95 +msgid "Past hour" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:103 +msgid "Past week" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:131 +msgid "Per-application totals for the last hour, aggregated on the router from the Netify Agent's classification. Select a row for the per-device breakdown." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:163 +msgid "Portal" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:187 +msgid "Printing" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:188 +msgid "Proxy" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:164 +msgid "Recreation" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:165 +msgid "Reference" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:166 +msgid "Remote Desktop" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:69 +msgid "Requires history storage (planned)" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:210 +msgid "Router" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:486 +msgid "Share" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1308 +msgid "Share of traffic by category" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:167 +msgid "Shopping" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:390 +msgid "Show all devices" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:868 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1284 +msgid "Show only %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:229 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:339 +msgid "Show per-device details" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:342 +msgid "Show per-device details for %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:892 +msgid "Show what %s is doing" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:361 +msgid "Showing the first %d applications; narrow the search to see the rest." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:168 +msgid "Social Media" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:297 +msgid "Sort by %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:169 +msgid "Sports" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:204 +msgid "Start typing a device name, MAC address or IP to find it. Click a device to see what it is doing in the applications list above. Press Clear to see all devices again." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:186 +msgid "Start typing an application or category name to show only that traffic, or click a category in the Categories panel. Prefix a word with a minus to exclude it instead, for example -netflix. Press Clear to see all traffic again." +msgstr "" + +#: applications/luci-app-appflow/root/usr/share/luci/menu.d/luci-app-appflow.json:25 +msgid "Statistics" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:170 +msgid "Streaming Media" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:171 +msgid "Technology" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:172 +msgid "Telco" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1364 +msgid "The AppFlow collector could not be reached over ubus, so there is no traffic data to show. Start the service and this page picks it up automatically." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:452 +msgid "The daemon could not return details for this application: %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:457 +msgid "This application is no longer in the daemon's aggregate table; it may have been pruned since the table was last refreshed." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:544 +msgid "This resets the per-application, per-device and per-category counters and time buckets. Live flow tracking continues uninterrupted, so the figures on this page simply start again from zero." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:107 +msgid "Top 10 apps by bandwidth usage" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:161 +msgid "Top applications" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:192 +msgid "Top devices" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:30 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:445 +msgid "Total" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1090 +msgid "Traffic over time" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1173 +msgid "Traffic per interval" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:173 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:693 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:438 +msgid "Unclassified" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:211 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:507 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:679 +msgid "Unknown" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:556 +msgid "Unknown device" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:279 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:159 +msgid "Updated %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1329 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:314 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:519 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:29 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:444 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:485 +msgid "Upload" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:316 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:519 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:659 +msgid "Uploaded" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:189 +msgid "VPN" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:175 +msgid "VPN & Proxy" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:174 +msgid "VoIP" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:190 +msgid "Web" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1361 +msgid "appflowd is not running" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1363 +msgid "appflowd is running, but this LuCI session was refused access to it over ubus (permission denied). This is an ACL problem, not a stopped service: confirm the luci-app-appflow ACL is installed and reload rpcd." +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:608 +msgid "by cumulative bytes" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:133 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:456 +msgid "by current rate" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:656 +msgid "just now" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:108 +msgid "last 60 minutes" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:607 +msgid "matching the filter, from flows in progress" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1426 +msgid "netifyd %s" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:335 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:473 +msgid "now" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:455 +msgid "on %s, from flows in progress" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:901 +msgid "router" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/statistics.js:313 +msgid "sum of all applications" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:143 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:190 +msgid "top, by cumulative bytes" +msgstr "" + +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/common.js:1311 +#: applications/luci-app-appflow/htdocs/luci-static/resources/view/appflow/overview.js:586 +msgid "total" +msgstr "" diff --git a/luci-app-appflow/po/zh-cn b/luci-app-appflow/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-appflow/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-appflow/po/zh_Hans/appflow.po b/luci-app-appflow/po/zh_Hans/appflow.po new file mode 100644 index 000000000..059a3b376 --- /dev/null +++ b/luci-app-appflow/po/zh_Hans/appflow.po @@ -0,0 +1,487 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-appflow\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_Hans\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +msgid "%d applications" +msgstr "%d 个应用" + +msgid "%d apps, %d devices" +msgstr "%d 个应用,%d 台设备" + +msgid "%d d ago" +msgstr "%d 天前" + +msgid "%d flows" +msgstr "%d 条流" + +msgid "%d h ago" +msgstr "%d 小时前" + +msgid "%d min ago" +msgstr "%d 分钟前" + +msgid "%d minute chart, %d second interval" +msgstr "%d 分钟图表,%d 秒间隔" + +msgid "%d of %d applications" +msgstr "共 %2$d 个应用中的 %1$d 个" + +msgid "%d s ago" +msgstr "%d 秒前" + +msgid "%d s rate window" +msgstr "%d 秒速率窗口" + +msgid "-%d min" +msgstr "-%d 分钟" + +msgid "1 flow" +msgstr "1 条流" + +msgid "12 buckets of 5 min" +msgstr "12 个 5 分钟区间" + +msgid "AI assistants" +msgstr "AI 助手" + +msgid "AI developer" +msgstr "AI 开发" + +msgid "AI infrastructure" +msgstr "AI 基础设施" + +msgid "AI media" +msgstr "AI 媒体" + +msgid "Adult" +msgstr "成人内容" + +msgid "Advertiser" +msgstr "广告" + +msgid "All traffic" +msgstr "全部流量" + +msgid "App traffic statistics" +msgstr "应用流量统计" + +msgid "AppFlow" +msgstr "应用流量" + +msgid "AppFlow also needs the Netify Agent running (%s), since that is where the per-application classification comes from." +msgstr "AppFlow 还需要 Netify Agent 处于运行状态(%s),因为应用分类数据正是来自该组件。" + +msgid "AppFlow statistics cleared." +msgstr "AppFlow 统计数据已清除。" + +msgid "AppFlow: access denied" +msgstr "AppFlow:访问被拒绝" + +msgid "Application" +msgstr "应用" + +msgid "Authentication" +msgstr "身份验证" + +msgid "Business" +msgstr "商务" + +msgid "CDN" +msgstr "CDN" + +msgid "Cancel" +msgstr "取消" + +msgid "Categories" +msgstr "分类" + +msgid "Clear" +msgstr "清除" + +msgid "Clear statistics" +msgstr "清除统计数据" + +msgid "Client" +msgstr "客户端" + +msgid "Close" +msgstr "关闭" + +msgid "Collecting data…" +msgstr "正在采集数据…" + +msgid "Collecting samples…" +msgstr "正在采集样本…" + +msgid "Collector state unknown" +msgstr "采集器状态未知" + +msgid "Collector uptime: %s" +msgstr "采集器运行时长:%s" + +msgid "Could not clear the statistics: %s" +msgstr "无法清除统计数据:%s" + +msgid "Current rate" +msgstr "当前速率" + +msgid "Cybersecurity" +msgstr "网络安全" + +msgid "Data usage" +msgstr "数据用量" + +msgid "Database" +msgstr "数据库" + +msgid "Details" +msgstr "详情" + +msgid "Device" +msgstr "设备" + +msgid "Device: %s" +msgstr "设备:%s" + +msgid "Devices" +msgstr "设备" + +msgid "Download" +msgstr "下载" + +msgid "Download: %s, upload: %s" +msgstr "下载:%s,上传:%s" + +msgid "Downloaded" +msgstr "已下载" + +msgid "Education" +msgstr "教育" + +msgid "Entertainment" +msgstr "娱乐" + +msgid "File Server" +msgstr "文件服务器" + +msgid "File Sharing" +msgstr "文件共享" + +msgid "Filter by application or category" +msgstr "按应用或分类筛选" + +msgid "Filter by device name, MAC or IP" +msgstr "按设备名称、MAC 或 IP 筛选" + +msgid "Financial" +msgstr "金融" + +msgid "Flows tracked: %d" +msgstr "已跟踪流数:%d" + +msgid "Gambling" +msgstr "博彩" + +msgid "Games" +msgstr "游戏" + +msgid "Government" +msgstr "政府" + +msgid "Grant access to per-application traffic statistics. The 'apps' method is granted because the overview drills into a device to show what it is doing; it returns an aggregate per device and application from the live flow table. 'flows' remains ungranted: that is the per-connection table, with remote addresses and ports." +msgstr "授予访问按应用流量统计数据的权限。由于概览页面会深入查看设备以显示其活动,因此授予了“apps”方法;该方法根据实时流量表返回按设备和应用汇总的数据。“flows”方法仍未授权,因为它提供的是包含远程地址和端口的逐连接数据。" + +msgid "Hosting" +msgstr "托管" + +msgid "Hosts" +msgstr "主机" + +msgid "Infrastructure" +msgstr "基础设施" + +msgid "IoT Devices" +msgstr "物联网设备" + +msgid "Last seen" +msgstr "最后活动" + +msgid "Live per-application traffic, classified on the router by the Netify Agent. Nothing on this page leaves the device." +msgstr "实时的应用级流量,由路由器上的 Netify Agent 进行分类。此页面的数据不会离开设备。" + +msgid "Live throughput" +msgstr "实时吞吐量" + +msgid "Mail" +msgstr "邮件" + +msgid "Malware" +msgstr "恶意软件" + +msgid "Media" +msgstr "媒体" + +msgid "Media Provider" +msgstr "媒体服务商" + +msgid "Messaging" +msgstr "即时通讯" + +msgid "Multicast / Broadcast" +msgstr "组播 / 广播" + +msgid "Netify agent connected" +msgstr "Netify Agent 已连接" + +msgid "Netify agent disconnected" +msgstr "Netify Agent 未连接" + +msgid "Networking" +msgstr "网络" + +msgid "News" +msgstr "新闻" + +msgid "No active devices." +msgstr "暂无活动设备。" + +msgid "No application matches \"%s\"." +msgstr "没有匹配“%s”的应用。" + +msgid "No application matches that filter." +msgstr "没有与该筛选条件匹配的应用。" + +msgid "No application traffic in the current window." +msgstr "当前时间窗口内没有应用流量。" + +msgid "No application traffic recorded in the last hour." +msgstr "最近一小时内没有记录到应用流量。" + +msgid "No categories yet." +msgstr "暂无分类。" + +msgid "No data" +msgstr "暂无数据" + +msgid "No device matches that filter." +msgstr "没有与该筛选条件匹配的设备。" + +msgid "No per-device breakdown available for this application." +msgstr "该应用暂无按设备的明细数据。" + +msgid "No time series available for this application." +msgstr "该应用暂无时间序列数据。" + +msgid "OS & Software Updates" +msgstr "操作系统与软件更新" + +msgid "Other" +msgstr "其他" + +msgid "Overview" +msgstr "概览" + +msgid "Past day" +msgstr "过去一天" + +msgid "Past hour" +msgstr "过去一小时" + +msgid "Past week" +msgstr "过去一周" + +msgid "Per-application totals for the last hour, aggregated on the router from the Netify Agent's classification. Select a row for the per-device breakdown." +msgstr "最近一小时内按应用汇总的流量总量,由路由器根据 Netify Agent 的分类结果统计得出。选择某一行可查看按设备的明细。" + +msgid "Portal" +msgstr "门户" + +msgid "Printing" +msgstr "打印" + +msgid "Proxy" +msgstr "代理" + +msgid "Recreation" +msgstr "休闲" + +msgid "Reference" +msgstr "参考资料" + +msgid "Remote Desktop" +msgstr "远程桌面" + +msgid "Requires history storage (planned)" +msgstr "需要历史数据存储(计划中)" + +msgid "Router" +msgstr "路由器" + +msgid "Share" +msgstr "占比" + +msgid "Share of traffic by category" +msgstr "各分类的流量占比" + +msgid "Shopping" +msgstr "购物" + +msgid "Show all devices" +msgstr "显示所有设备" + +msgid "Show only %s" +msgstr "仅显示 %s" + +msgid "Show per-device details" +msgstr "显示按设备的详情" + +msgid "Show per-device details for %s" +msgstr "显示 %s 的设备明细" + +msgid "Show what %s is doing" +msgstr "显示 %s 的活动" + +msgid "Showing the first %d applications; narrow the search to see the rest." +msgstr "仅显示前 %d 个应用;请缩小搜索范围以查看其余应用。" + +msgid "Social Media" +msgstr "社交媒体" + +msgid "Sort by %s" +msgstr "按 %s 排序" + +msgid "Sports" +msgstr "体育" + +msgid "Start typing a device name, MAC address or IP to find it. Click a device to see what it is doing in the applications list above. Press Clear to see all devices again." +msgstr "输入设备名称、MAC 地址或 IP 即可查找设备。点击设备可在上方应用列表中查看其活动。点击“清除”可再次显示所有设备。" + +msgid "Start typing an application or category name to show only that traffic, or click a category in the Categories panel. Prefix a word with a minus to exclude it instead, for example -netflix. Press Clear to see all traffic again." +msgstr "输入应用或分类名称以仅显示相应流量,也可点击“分类”面板中的分类。在词语前添加减号可将其排除,例如 -netflix。点击“清除”可再次显示全部流量。" + +msgid "Statistics" +msgstr "统计" + +msgid "Streaming Media" +msgstr "流媒体" + +msgid "Technology" +msgstr "科技" + +msgid "Telco" +msgstr "电信" + +msgid "The AppFlow collector could not be reached over ubus, so there is no traffic data to show. Start the service and this page picks it up automatically." +msgstr "无法通过 ubus 连接到 AppFlow 采集器,因此没有可显示的流量数据。启动该服务后,此页面会自动获取数据。" + +msgid "The daemon could not return details for this application: %s" +msgstr "守护进程未能返回该应用的详情:%s" + +msgid "This application is no longer in the daemon's aggregate table; it may have been pruned since the table was last refreshed." +msgstr "该应用已不在守护进程的汇总表中;自上次刷新以来,该记录可能已被清理。" + +msgid "This resets the per-application, per-device and per-category counters and time buckets. Live flow tracking continues uninterrupted, so the figures on this page simply start again from zero." +msgstr "此操作会重置按应用、按设备和按分类的计数器与时间分桶。实时流量跟踪不会中断,此页面上的数字只是重新从零开始计算。" + +msgid "Top 10 apps by bandwidth usage" +msgstr "按带宽用量排名前 10 的应用" + +msgid "Top applications" +msgstr "热门应用" + +msgid "Top devices" +msgstr "热门设备" + +msgid "Total" +msgstr "总计" + +msgid "Traffic over time" +msgstr "流量趋势" + +msgid "Traffic per interval" +msgstr "每个时间区间的流量" + +msgid "Unclassified" +msgstr "未分类" + +msgid "Unknown" +msgstr "未知" + +msgid "Unknown device" +msgstr "未知设备" + +msgid "Updated %s" +msgstr "更新于 %s" + +msgid "Upload" +msgstr "上传" + +msgid "Uploaded" +msgstr "已上传" + +msgid "VPN" +msgstr "VPN" + +msgid "VPN & Proxy" +msgstr "VPN 与代理" + +msgid "VoIP" +msgstr "VoIP" + +msgid "Web" +msgstr "网页" + +msgid "appflowd is not running" +msgstr "appflowd 未运行" + +msgid "appflowd is running, but this LuCI session was refused access to it over ubus (permission denied). This is an ACL problem, not a stopped service: confirm the luci-app-appflow ACL is installed and reload rpcd." +msgstr "appflowd 正在运行,但当前 LuCI 会话通过 ubus 访问它时被拒绝(权限不足)。这是 ACL 权限问题,而非服务未启动:请确认已安装 luci-app-appflow 的 ACL 并重新加载 rpcd。" + +msgid "by cumulative bytes" +msgstr "按累计字节数" + +msgid "by current rate" +msgstr "按当前速率" + +msgid "just now" +msgstr "刚刚" + +msgid "last 60 minutes" +msgstr "最近 60 分钟" + +msgid "matching the filter, from flows in progress" +msgstr "匹配筛选条件,来自进行中的流" + +msgid "netifyd %s" +msgstr "netifyd %s" + +msgid "now" +msgstr "现在" + +msgid "on %s, from flows in progress" +msgstr "在 %s 上,来自进行中的流" + +msgid "router" +msgstr "本机" + +msgid "sum of all applications" +msgstr "所有应用的总和" + +msgid "top, by cumulative bytes" +msgstr "按累计字节数排名" + +msgid "total" +msgstr "总计" + +# The msgids below no longer exist in the template. Kept as +# obsolete rather than deleted, which is what msgmerge does, +# so a reworded string can be recovered by hand. +#~ msgid "Grant access to per-application traffic statistics" +#~ msgstr "授予访问按应用流量统计数据的权限" + +#~ msgid "Search app name" +#~ msgstr "搜索应用名称" diff --git a/luci-app-appflow/root/etc/config/appflow b/luci-app-appflow/root/etc/config/appflow new file mode 100644 index 000000000..d6147497c --- /dev/null +++ b/luci-app-appflow/root/etc/config/appflow @@ -0,0 +1,68 @@ +config appflow 'main' + # Netify Agent JSON export socket (netifyd.conf: [socket] listen_path[0]). + option socket_path '/var/run/netifyd/netifyd.sock' + + # Maximum concurrently tracked flows. The flow table is the daemon's only + # significant memory consumer; 4096 flows is roughly 3 MB of state. + option flow_max '4096' + + # Sliding window, in seconds, behind the reported rates. netifyd delivers + # per-flow counters about every 15 s, so values below ~20 s make the rates + # jump between burst and idle rather than making them more responsive. + option rate_window '30' + + # Default number of rows returned by the summary method. + option top_n '10' + + # Drop a flow from the table after this many seconds without an update. + option idle_timeout '300' + + # Break AI services out as their own applications and category. + # + # netifyd cannot classify them: its signature set is dated August 2023 and + # contains no AI vendors at all, so this traffic arrives as generic HTTP/S. + # The TLS SNI is already in the flow record, so appflowd matches it against + # a built-in table of AI service domains and relabels the flow. + # + # This is HOSTNAME MATCHING, NOT DPI. It asserts a classification netifyd + # did not make. netifyd's own answer always wins except for a handful of + # services whose parent brand netifyd recognises (Gemini under Google, + # Copilot under GitHub), so this falls silent by itself if Netify ever ship + # AI signatures. It changes identity only and never affects byte accounting. + # + # 0 disables it entirely. + option ai_breakout '1' + +# --------------------------------------------------------------------------- +# Teaching AppFlow a service it does not know +# --------------------------------------------------------------------------- +# +# netifyd's signature set is vendor data and it goes stale; the copy shipped +# with OpenWrt is dated August 2023. AppFlow recognises a built-in list of +# services by the server name their TLS connections announce, and a `hostmap` +# section adds to that list or overrides it. +# +# Each section teaches one domain suffix: +# +# config hostmap +# option suffix 'torproject.org' +# option name 'Tor' +# option category 'Privacy' +# +# suffix matched against the last two, three or four labels of the server +# name, anchored on a label boundary. 'torproject.org' matches +# 'www.torproject.org' and 'check.torproject.org' but NOT +# 'torproject.org.example.com'. Must contain a dot. +# name what appears in the application list. +# category what appears in the category breakdown. Free text; reuse an +# existing one to group with it, or invent your own. +# strong optional, default 0. 0 means netifyd's own identification wins +# where it has one, and this only fills a gap. 1 means override +# netifyd too, which is what you want when netifyd recognises a +# parent brand and swallows the service inside it. +# +# Apply with: /etc/init.d/appflowd restart +# Check it took: logread -e appflowd | grep hostmap +# +# The same section overrides a built-in entry, so this is also how you correct +# one that is wrong for your network. diff --git a/luci-app-appflow/root/etc/init.d/appflowd b/luci-app-appflow/root/etc/init.d/appflowd new file mode 100755 index 000000000..38a64d650 --- /dev/null +++ b/luci-app-appflow/root/etc/init.d/appflowd @@ -0,0 +1,34 @@ +#!/bin/sh /etc/rc.common +# shellcheck disable=SC2034 # START/STOP/USE_PROCD/PROG are read by +# /etc/rc.common, which sources this file; they are never referenced here. +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2026 VolanticSystems + +# After netifyd (START=50): appflowd tolerates the agent being absent and +# retries, but starting later avoids a pointless first reconnect cycle. +START=95 +STOP=10 + +USE_PROCD=1 +PROG=/usr/sbin/appflowd + +start_service() { + procd_open_instance appflowd + procd_set_param command "$PROG" + # Keep restarting on unexpected exit: 5 s settle, give up after 5 crashes + # inside an hour so a genuinely broken build does not spin forever. + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/appflowd.pid + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger "appflow" +} + +reload_service() { + # Configuration is read once at start-up; a reload is a restart. + restart +} diff --git a/luci-app-appflow/root/usr/sbin/appflowd b/luci-app-appflow/root/usr/sbin/appflowd new file mode 100755 index 000000000..33ae3b0e4 --- /dev/null +++ b/luci-app-appflow/root/usr/sbin/appflowd @@ -0,0 +1,2545 @@ +#!/usr/bin/ucode +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 VolanticSystems +// +// appflowd — aggregate Netify Agent (netifyd) DPI flow events into live +// per-application / per-device / per-category traffic statistics and serve +// them over ubus. See docs/DESIGN.md; section references below point there. +// +// Data path (§3): +// netifyd --unix stream, length-framed JSON--> appflowd --ubus--> LuCI +// +// Everything that counts a byte goes through account() (§7.1), which is the +// single seam historical accounting will tap into. + +import * as fs from "fs"; +import * as uloop from "uloop"; +import * as ubus from "ubus"; +import * as socket from "socket"; +import { cursor } from "uci"; + +/* ------------------------------------------------------------------ * + * Constants — the memory bounds of §3.3 live here. Every table in the + * daemon is capped; nothing grows with uptime or traffic volume. + * ------------------------------------------------------------------ */ + +const NAME = "appflowd"; +const VERSION = "1.0"; + +const RX_CHUNK = 16384; // bytes per recv() +const RX_MAX = 262144; // partial-line buffer ceiling; beyond this the + // peer is not speaking our protocol -> resync +const RX_DRAIN_MAX = 64; // recv() calls per readable event (fairness) + +const AGG_MAX = 384; // max aggregates per class (apps/devices/cats) +const APP_DEV_MAX = 64; // devices tracked per application (LRU) +const APP_HOST_MAX = 16; // hostnames tracked per application (LRU) +const NAME_MAX = 8192; // id->tag map ceiling (nDPI catalogue is ~2.5k) +const TAG_MAX = 64; // max stored length of any detection tag +const UNKNOWN_TYPE_MAX = 16; // distinct unhandled event types remembered +const EVENT_TYPE_MAX = 24; // distinct event types counted in status +const FLOWS_REPLY_MAX = 200; // hard ceiling for the flows[] ubus reply +const LIST_REPLY_MAX = 200; // hard ceiling for apps[]/devices[] replies + +// Trailing-hour time series (§7.2 buckets, GL-parity `stats` method): +// 12 slots x 5 minutes. Slot numbers are absolute (epoch_ms / SERIES_MS), so +// a slot is self-describing and stale slots need no eager clearing. +const SERIES_SLOTS = 12; +const SERIES_MS = 300000; +// Per (app x device) we only need trailing-hour *totals*, not a series, so a +// coarser 4 x 15 min ring is used: same code path, quarter the memory. The +// window it reports is therefore 45-60 minutes rather than exactly 60. +const DEVSER_SLOTS = 4; +const DEVSER_MS = 900000; + +const PRUNE_MS = 15000; // idle-flow / aggregate sweep interval +const LEASE_TTL_MS = 30000; // DHCP lease file re-read throttle (§4) +const LOG_REPEAT_MS = 60000; // identical-message log suppression window +const RECONNECT_MIN_MS = 1000; +const RECONNECT_MAX_MS = 30000; + +const CATEGORY_FILE = "/etc/netify.d/netify-categories.json"; +const LEASE_FILE = "/tmp/dhcp.leases"; +const STATUS_FILE = "/var/run/netifyd/status.json"; +const CONNTRACK_FILE = "/proc/net/nf_conntrack"; + +const CT_TTL_MS = 2000; // reuse one conntrack snapshot this long (3.2) +const CT_RETRY_MS = 60000; // re-probe an unreadable conntrack this often +const CT_FORCE_MIN_MS = 250; // floor between miss-driven refreshes (3.2) +const CT_MAX = 8192; // entries kept from one snapshot (memory bound, 3.3) + +const CFG_DEFAULTS = { + socket_path: "/var/run/netifyd/netifyd.sock", + conntrack_path: "/proc/net/nf_conntrack", + flow_max: 4096, + rate_window: 30, + top_n: 10, + idle_timeout: 300, + ai_breakout: 1 +}; + +// Conntrack snapshot used to resolve NAT twins (3.2). `map` is keyed on the +// REPLY tuple, which is the view netifyd's external capture reports, and holds +// true when that flow was source-translated. `avail` is null until first +// probed, then true/false; false is re-probed every CT_RETRY_MS so a conntrack +// module loaded after us is picked up without needing a restart. +let conntrack = { at: 0, probed: 0, avail: null, map: {}, entries: 0 }; + +// Any event type matching this and carrying flow.digest finalises a flow. +// netifyd 4.4.7 emits "flow_purge"; the regex keeps a renamed or additional +// event working without a code change (the dispatcher below is data-driven). +const PURGE_RE = regexp("(purge|expire|delete|remove|terminat|close)", "i"); + +/* ------------------------------------------------------------------ * + * State + * ------------------------------------------------------------------ */ + +let cfg = {}; + +let sock = null; // socket handle to netifyd +let sock_fd = null; // uloop fd handle +let rxbuf = ""; // partial line carry-over +let connected = false; +let ever_connected = false; // distinguishes the first connect from reconnects +let backoff_ms = RECONNECT_MIN_MS; +let retry_timer = null; + +let flows = {}; // digest -> flow record +let nflows = 0; +// True once any internal (LAN-bridge) capture has been seen. The single-count +// shadow rule only trusts "the internal capture already counted this" when +// internal capture is actually active; on an external-only netifyd setup it +// must not shadow client flows or the dashboard would read near-zero. +let saw_internal = false; + +let apps = {}, devs = {}, cats = {}; +let totals = null; // aggregate covering everything + +let names = { app: {}, proto: {} }; // id -> netifyd tag (definitions event) +let catnames = { app: {}, proto: {} }; // id -> category tag (categories file) + +let leases = { at: 0, mtime: -1, by_mac: {}, by_ip: {} }; +let router = { macs: {}, ips: {} }; // this router's own L2/L3 identities +let agent = { version: null, uptime: 0, flow_count: 0, ts: 0 }; + +let stats = { + events: 0, frames: 0, malformed: 0, unknown: 0, unknown_types: {}, + by_type: {}, reconnects: 0, connect_fail: 0, purges: 0, + flows_seen: 0, flows_late: 0, flows_pruned: 0, flows_dropped: 0, + // Flows relabelled by the AI hostname table. Observable so a test can + // assert the feature fired rather than inferring it from a label. + ai_labeled: 0, + // Flows evicted because the table was AT ITS CAP, as opposed to evicted for + // going idle. The two used to share flows_pruned, where cap pressure was + // invisible underneath ordinary housekeeping. + // + // This exists because flows_dropped, which reads like the counter for + // "flows we could not keep", CANNOT BE MOVED BY ANY INPUT. flow_new() + // increments it only if the table is still full after prune(now, true), and + // that prune unconditionally sheds int(flow_max/10)+1 entries, so it always + // frees space and the fall-through never happens. Measured 2026-08-26 by + // tests/protocol-suite.sh: 200 distinct digests against flow_max=64 gave + // tracked=60 pruned=143 dropped=0. An operator reading `dropped: 0` + // concluded nothing was being lost while flows were being shed constantly. + // Same shape as aggs_refused below, and found the same way. + // + // flows_dropped is kept as the genuine last-resort backstop it always was. + // flows_shed is the number an operator should actually watch. + flows_shed: 0, + shadowed: 0, aggs_pruned: 0, rx_bytes: 0, resyncs: 0, sweeps: 0, + // Times an aggregate table was full of LIVE entries and a new key was + // therefore not tracked at all. Distinct from aggs_pruned, which counts + // evictions of idle entries and is normal housekeeping. A non-zero value + // here means the dashboard is silently incomplete. + aggs_refused: 0, + no_total: 0, last_event_ms: 0, last_reset_ms: 0, + // NAT twin resolution outcomes (3.2): twins shadowed on conntrack evidence, + // router-originated flows kept, and flows conntrack could not decide, which + // are shadowed on the safe default. + ct_twins: 0, ct_local: 0, ct_unresolved: 0, + // Purge-only flows rescued (see flow_update). Expected to stay 0 on + // netifyd 4.4.7; a non-zero value means that assumption has changed. + purge_only: 0, + // Byte conservation (§2.5): reported == attributed + shadowed, or + // something is leaking. Both silent byte-loss defects found in this + // daemon would have been visible here from the start. + bytes_reported: 0, bytes_attributed: 0, bytes_shadowed: 0 +}; + +let start_ms = 0; +let start_mono = 0; +let log_seen = {}; +// The published ubus object must stay referenced: dropping the last reference +// lets ucode collect it, which unregisters the object from ubus. +let ubus_obj = null; + +// Forward declarations. ucode binds an identifier at compile time: one that +// is still undeclared resolves to a global, while `function f()` further down +// declares a local, so the call would find null. connect/schedule_reconnect +// are mutually recursive and prune() is used by the flow table above its own +// definition, so those four are declared here and assigned below. +let prune, connect, disconnect, schedule_reconnect; + +/* ------------------------------------------------------------------ * + * Small helpers + * ------------------------------------------------------------------ */ + +function now_ms() { + let c = clock(); + return c[0] * 1000 + int(c[1] / 1000000); +} + +function mono_ms() { + let c = clock(true); + return c[0] * 1000 + int(c[1] / 1000000); +} + +function num(v, def) { + if (v == null) + return def; + let n = int(v); + return (type(n) == "int") ? n : def; +} + +function clampi(v, lo, hi) { + return (v < lo) ? lo : ((v > hi) ? hi : v); +} + +// procd feeds our stdout to logd as daemon.info and our stderr as daemon.err +// (see the init script), so route by severity and flush explicitly -- stdout +// is block buffered when it is a pipe. +function logmsg(level, fmt, ...args) { + let out = (level == "error") ? fs.stderr : fs.stdout; + out.write(sprintf("[%s] %s\n", level, sprintf(fmt, ...args))); + out.flush(); +} + +// Log at most one identical message per LOG_REPEAT_MS. Reconnect churn while +// netifyd is down must not turn into log spam. +function logonce(key, level, fmt, ...args) { + let now = mono_ms(); + if (log_seen[key] && (now - log_seen[key]) < LOG_REPEAT_MS) + return; + if (length(log_seen) > 32) + log_seen = {}; + log_seen[key] = now; + logmsg(level, fmt, ...args); +} + +/* ------------------------------------------------------------------ * + * Configuration (uci: /etc/config/appflow) + * ------------------------------------------------------------------ */ + +function cfg_load() { + let s = {}; + try { + s = cursor().get_all("appflow", "main") ?? {}; + } catch (e) { + logmsg("warn", "uci: %s — using defaults", e); + } + + /* User-supplied hostname mappings, from `config hostmap` sections. + * + * Each adds or OVERRIDES one entry in the built-in table, so this is both + * the way to teach appflow a service it does not know and the way to + * correct one it gets wrong. Consulted before the built-in table. + * + * config hostmap + * option suffix 'torproject.org' + * option name 'Tor' + * option category 'Privacy' + * option strong '0' + * + * A malformed section is skipped and named in the log rather than + * silently ignored: a typo in a suffix produces a rule that matches + * nothing, which looks identical to the feature not working. + */ + let hostmap = {}; + let bad = 0; + try { + cursor().foreach("appflow", "hostmap", function(h) { + let suf = lc(trim(h.suffix ?? "")); + let nm = trim(h.name ?? ""); + + /* A suffix must contain a dot, or it can never match: the matcher + * rebuilds the last N labels of a hostname and compares, so a + * single label matches nothing. */ + if (!length(suf) || !length(nm) || index(suf, ".") < 0) { + bad++; + logmsg("warn", "hostmap: skipping entry with suffix %s name %s", + suf ?? "(empty)", nm ?? "(empty)"); + + return; + } + + hostmap[suf] = [ nm, + length(trim(h.category ?? "")) ? trim(h.category) : "other", + num(h.strong, 0) != 0 ]; + }); + } catch (e) { + logmsg("warn", "hostmap: %s", e); + } + if (length(hostmap)) + logmsg("info", "hostmap: %d user entr%s loaded%s", + length(hostmap), length(hostmap) == 1 ? "y" : "ies", + bad ? sprintf(", %d skipped", bad) : ""); + + cfg = { + socket_path: length(s.socket_path ?? "") ? s.socket_path : CFG_DEFAULTS.socket_path, + conntrack_path: length(s.conntrack_path ?? "") ? s.conntrack_path : CFG_DEFAULTS.conntrack_path, + flow_max: clampi(num(s.flow_max, CFG_DEFAULTS.flow_max), 64, 65536), + rate_window: clampi(num(s.rate_window, CFG_DEFAULTS.rate_window), 5, 600), + top_n: clampi(num(s.top_n, CFG_DEFAULTS.top_n), 1, 100), + idle_timeout: clampi(num(s.idle_timeout, CFG_DEFAULTS.idle_timeout), 15, 86400), + // Absent means ON. A dashboard feature that defaults to off is a + // feature nobody sees, and this one only ever relabels flows netifyd + // had no answer for. + ai_breakout: num(s.ai_breakout, CFG_DEFAULTS.ai_breakout) != 0, + hostmap: hostmap + }; + cfg.rate_ms = cfg.rate_window * 1000; + cfg.idle_ms = cfg.idle_timeout * 1000; +} + +/* ------------------------------------------------------------------ * + * Time-bucketed accumulators (§7.2) + * + * A ring of `n` slots, each tagged with its absolute slot number. Writes are + * O(1) and self-invalidating: a slot whose tag is not in the trailing window + * is simply not counted, so no eager rotation is needed for correctness. The + * periodic sweep below still clears stale slots so their bytes cannot be + * resurrected by a clock step and so status output stays cheap. + * ------------------------------------------------------------------ */ + +function ring_new(n) { + let r = { s: [], d: [], u: [] }; + for (let i = 0; i < n; i++) { + push(r.s, -1); + push(r.d, 0); + push(r.u, 0); + } + return r; +} + +function ring_add(r, slot, n, up, down) { + let i = slot % n; + if (r.s[i] != slot) { + r.s[i] = slot; + r.d[i] = 0; + r.u[i] = 0; + } + r.d[i] += down; + r.u[i] += up; +} + +function ring_total(r, slot, n) { + let d = 0, u = 0; + for (let i = 0; i < n; i++) { + if (r.s[i] > (slot - n) && r.s[i] <= slot) { + d += r.d[i]; + u += r.u[i]; + } + } + return [ d, u ]; +} + +function ring_series(r, slot, n, slot_ms) { + let out = []; + for (let k = n - 1; k >= 0; k--) { + let s = slot - k, i = s % n, ok = (r.s[i] == s); + push(out, { + ts: s * slot_ms, + download: ok ? r.d[i] : 0, + upload: ok ? r.u[i] : 0 + }); + } + return out; +} + +function ring_sweep(r, slot, n) { + for (let i = 0; i < n; i++) + if (r.s[i] >= 0 && (r.s[i] <= (slot - n) || r.s[i] > slot)) { + r.s[i] = -1; + r.d[i] = 0; + r.u[i] = 0; + } +} + +function ring_clear(r, n) { + for (let i = 0; i < n; i++) { + r.s[i] = -1; + r.d[i] = 0; + r.u[i] = 0; + } +} + +/* ------------------------------------------------------------------ * + * Aggregates + * + * Shape is {cur, buckets} per §7.2: `cur` is the session accumulator plus a + * two-bucket sliding window used for the live rate, `buckets` is the hour-long + * time series. History support (§7) extends `buckets`, nothing else. + * ------------------------------------------------------------------ */ + +function agg_new(kind, key, name, now) { + return { + kind, key, name, + cur: { up: 0, down: 0, flows: 0, first: now, last: 0 }, + // Sliding rate window: bytes land in the open bucket; when it ages past + // rate_window it becomes the closed bucket and a fresh one opens. The + // reported rate spans closed+open, i.e. 1x-2x the window. Chosen over + // an EWMA because netifyd delivers flow_stats in ~15 s bursts: an EWMA + // tuned tight enough to be live would oscillate between spike and zero, + // while this integrates each burst over a known interval. Cost is O(1) + // memory and no per-second timer. + rate: { t0: now, up: 0, down: 0, pdt: 0, pup: 0, pdown: 0 }, + buckets: ring_new(SERIES_SLOTS) + }; +} + +function agg_roll(a, now) { + let span = now - a.rate.t0; + if (span >= cfg.rate_ms) { + a.rate.pdt = span; + a.rate.pup = a.rate.up; + a.rate.pdown = a.rate.down; + a.rate.t0 = now; + a.rate.up = 0; + a.rate.down = 0; + } +} + +// Pure: computes the rate WITHOUT rolling the window, so a read never mutates +// state. Two clients polling at different intervals (or a second browser tab) +// must see identical numbers for identical traffic; the window is advanced only +// by agg_bump and housekeeping. Rolls are applied virtually here. +function agg_rate(a, now) { + let span = now - a.rate.t0; + let pdt, pup, pdown, cspan, cup, cdown; + if (span >= cfg.rate_ms) { // window would have rolled + pdt = span; pup = a.rate.up; pdown = a.rate.down; + cspan = 0; cup = 0; cdown = 0; + } else { + pdt = a.rate.pdt; pup = a.rate.pup; pdown = a.rate.pdown; + cspan = span; cup = a.rate.up; cdown = a.rate.down; + } + // Floor the denominator at one window so a single burst cannot be reported + // as an impossibly high instantaneous rate. + let denom = pdt + cspan; + if (denom < cfg.rate_ms) + denom = cfg.rate_ms; + return [ + int((pup + cup) * 1000 / denom), + int((pdown + cdown) * 1000 / denom) + ]; +} + +function agg_bump(a, up, down, now, slot) { + agg_roll(a, now); + a.cur.up += up; + a.cur.down += down; + a.cur.last = now; + a.rate.up += up; + a.rate.down += down; + ring_add(a.buckets, slot, SERIES_SLOTS, up, down); +} + +function agg_reset(a, now) { + a.cur.up = 0; + a.cur.down = 0; + a.cur.first = now; + a.cur.last = 0; + a.rate = { t0: now, up: 0, down: 0, pdt: 0, pup: 0, pdown: 0 }; + ring_clear(a.buckets, SERIES_SLOTS); +} + +// Evict the least recently active zero-flow aggregate when a class is full. +function agg_make(tbl, kind, key, name, now) { + let a = tbl[key]; + if (a) + return a; + + if (length(tbl) >= AGG_MAX) { + let victim = null, oldest = now + 1; + for (let k in tbl) { + let c = tbl[k]; + if (c.cur.flows > 0) + continue; + if (c.cur.last < oldest) { + oldest = c.cur.last; + victim = k; + } + } + if (victim == null) { + // Every slot is backed by a live flow: refuse to grow (§3.3). + // Bytes still land in the totals, so the summary stays truthful. + // Counted, not just logged. logonce() fires ONCE for the life of + // the process, so without this an operator polling status sees + // `devices: 384` and no sign that anything is being dropped -- the + // table looks healthy precisely when it has stopped accepting the + // device the user is looking for. A security panel on 2026-08-25 + // converged on this table being fillable by a LAN attacker + // (confirmed: six spoofed MACs produced six aggregates on + // hardware). Evicting live data is already refused above, which is + // the right call; what was missing was any way to SEE the refusal. + stats.aggs_refused++; + logonce("aggfull", "warn", "%s table full (%d), not tracking %s", + kind, AGG_MAX, key); + return null; + } + delete tbl[victim]; + stats.aggs_pruned++; + } + + a = agg_new(kind, key, name, now); + tbl[key] = a; + return a; +} + +/* ------------------------------------------------------------------ * + * Per-application breakdowns (bounded): device totals and hostnames. + * ------------------------------------------------------------------ */ + +function detail_dev(a, fr, up, down, now, dslot) { + if (!a.devs) + a.devs = {}; + let e = a.devs[fr.dev_key]; + if (!e) { + if (length(a.devs) >= APP_DEV_MAX) { + // Seed from the first candidate, not from `now`: a backwards clock + // step (routine on an RTC-less router after its first NTP sync) can + // push every stored `last` past now, which would leave victim null + // and grow the map unbounded. + let victim = null, oldest = null; + for (let k in a.devs) + if (oldest == null || a.devs[k].last < oldest) { + oldest = a.devs[k].last; + victim = k; + } + if (victim == null) + return; // cannot make room; do not exceed the cap + delete a.devs[victim]; + } + e = { mac: fr.dev_mac, ip: fr.dev_ip, last: now, ring: ring_new(DEVSER_SLOTS) }; + a.devs[fr.dev_key] = e; + } + if (fr.dev_ip) + e.ip = fr.dev_ip; + e.last = now; + ring_add(e.ring, dslot, DEVSER_SLOTS, up, down); +} + +function detail_host(a, fr, up, down, now) { + if (!fr.host) + return; + if (!a.hosts) + a.hosts = {}; + let e = a.hosts[fr.host]; + if (!e) { + if (length(a.hosts) >= APP_HOST_MAX) { + let victim = null, oldest = null; + for (let k in a.hosts) + if (oldest == null || a.hosts[k].last < oldest) { + oldest = a.hosts[k].last; + victim = k; + } + if (victim == null) + return; + delete a.hosts[victim]; + } + // Same trailing-window ring as the device breakdown, so the two tables + // in the detail drawer report over the same interval and stay comparable. + e = { last: now, ring: ring_new(DEVSER_SLOTS) }; + a.hosts[fr.host] = e; + } + e.last = now; + ring_add(e.ring, int(now / DEVSER_MS), DEVSER_SLOTS, up, down); +} + +/* ------------------------------------------------------------------ * + * Persistence interface (§7.3) + * + * v1 ships this dormant on purpose. A history implementation (RAM-rotating + * file, collectd exec, sqlite on USB) plugs in here without touching the + * accounting path. Flash-wear rule: coarse buckets only, never per sample. + * ------------------------------------------------------------------ */ + +const store = { + flush: function(_aggregates, _slot) { + // no-op in v1 + } +}; + +/* ------------------------------------------------------------------ * + * THE accounting choke point (§7.1) + * + * Every byte appflowd ever counts passes through here, exactly once. + * `d_local` / `d_other` are netifyd's per-update directional deltas; the flow + * record knows which of the two endpoints is the attributed device, so: + * upload = bytes sent BY the device (device side -> peer) + * download = bytes sent TO the device (peer -> device side) + * ------------------------------------------------------------------ */ + +function account(fr, d_local, d_other, ts) { + if (fr.shadow) { // deduplicated NAT twin (§3.2) + // Deliberately dropped, not lost. Counted so that + // bytes_reported == bytes_attributed + bytes_shadowed holds, and any + // residual is a real leak rather than the dedup doing its job. + let s = d_local + d_other; + if (s > 0) + stats.bytes_shadowed += s; + return; + } + + let up = fr.dev_local ? d_local : d_other; + let down = fr.dev_local ? d_other : d_local; + if (up < 0) up = 0; + if (down < 0) down = 0; + + stats.bytes_attributed += up + down; + fr.up += up; + fr.down += down; + fr.seen = ts; + + if (up == 0 && down == 0) + return; + + let slot = int(ts / SERIES_MS); + let dslot = int(ts / DEVSER_MS); + + agg_bump(totals, up, down, ts, slot); + if (fr.a) { + agg_bump(fr.a, up, down, ts, slot); + detail_dev(fr.a, fr, up, down, ts, dslot); + detail_host(fr.a, fr, up, down, ts); + } + if (fr.d) + agg_bump(fr.d, up, down, ts, slot); + if (fr.c) + agg_bump(fr.c, up, down, ts, slot); + + // History (§7) taps in exactly here: the buckets these bumps fill are what + // store.flush() drains once a persistent implementation exists. +} + +/* ------------------------------------------------------------------ * + * Naming (§2.3) + * ------------------------------------------------------------------ */ + +// Post-capitalization fixes for tokens where naive Title Case reads wrong. +const BRAND_CAPS = { + "Openwrt": "OpenWrt", "Dns": "DNS", "Http": "HTTP", "Https": "HTTPS", + "Ssl": "SSL", "Tls": "TLS", "Ssh": "SSH", "Ntp": "NTP", "Vpn": "VPN", + "Cdn": "CDN", "Voip": "VoIP", "Tv": "TV", "Api": "API", "Ip": "IP", + "Iot": "IoT", "Ai": "AI", "Ads": "Ads", + // Brands whose own capitalization is word-internal, which single-letter + // Title Case cannot produce. + "Youtube": "YouTube", "Github": "GitHub", "Gitlab": "GitLab", + "Whatsapp": "WhatsApp", "Tiktok": "TikTok", "Paypal": "PayPal", + "Icloud": "iCloud", "Iphone": "iPhone", "Deepl": "DeepL", + "Openai": "OpenAI", "Openvpn": "OpenVPN", "Wireguard": "WireGuard", + "Bittorrent": "BitTorrent", "Speedtest": "Speedtest", "Steamos": "SteamOS" +}; + +function title_case(s) { + let out = []; + for (let w in split(s, " ")) { + let t = length(w) ? (uc(substr(w, 0, 1)) + substr(w, 1)) : w; + push(out, BRAND_CAPS[t] ?? t); + } + return join(" ", out); +} + +// Netify application tags look like "netify.google-ads"; nDPI protocol tags +// are already display-styled ("HTTP/S", "ZOOM", "WireGuard") and are kept +// verbatim. +function display_name(tag) { + if (!length(tag ?? "")) + return null; + if (substr(tag, 0, 7) != "netify.") + return tag; + let slug = substr(tag, 7); + slug = replace(slug, /[-_.]/g, " "); + return title_case(slug); +} + +function load_categories() { + let raw; + try { + raw = fs.readfile(CATEGORY_FILE); + } catch (e) { + raw = null; + } + if (!raw) { + logonce("cats", "warn", "category names unavailable: %s", CATEGORY_FILE); + return; + } + + let obj; + try { + obj = json(raw); + } catch (e) { + logmsg("warn", "%s: parse error: %s", CATEGORY_FILE, e); + return; + } + + // The file maps tag -> id; we need id -> tag. The far larger + // application_index / protocol_index member lists are deliberately not + // retained: flow events carry the category id directly. + for (let t in obj.application_tag_index ?? {}) + catnames.app[obj.application_tag_index[t]] = t; + for (let t in obj.protocol_tag_index ?? {}) + catnames.proto[obj.protocol_tag_index[t]] = t; +} + +function load_leases(now) { + if (leases.at && (now - leases.at) < LEASE_TTL_MS) + return; + leases.at = now; + + let st; + try { + st = fs.stat(LEASE_FILE); + } catch (e) { + st = null; + } + if (!st) { + leases.mtime = -1; + return; + } + if (st.mtime == leases.mtime) + return; + leases.mtime = st.mtime; + + let raw; + try { + raw = fs.readfile(LEASE_FILE); + } catch (e) { + return; + } + if (!raw) + return; + + let by_mac = {}, by_ip = {}; + for (let line in split(raw, "\n")) { + let f = split(trim(line), /[ \t]+/); + // + if (length(f) < 4 || f[3] == "*") + continue; + by_mac[lc(f[1])] = f[3]; + by_ip[f[2]] = f[3]; + } + leases.by_mac = by_mac; + leases.by_ip = by_ip; +} + +function device_name(key, mac, ip, now) { + if (key == "router") + return "Router"; + if (key == "unknown") + return "Unknown"; + if (key == "multicast") + return "Multicast / Broadcast"; + load_leases(now); + return leases.by_mac[mac ?? ""] ?? leases.by_ip[ip ?? ""] ?? (mac ?? ip ?? key); +} + +/* ------------------------------------------------------------------ * + * Router identity — needed to tell "this box" from a client (§3.2). + * ------------------------------------------------------------------ */ + +// Rebuild the router-identity maps wholesale from an interfaces block. An +// agent_status frame that carries `interfaces` is complete and authoritative, +// so replacing rather than merging keeps these maps bounded (IPv6 privacy +// addresses and WAN re-leases would otherwise accumulate stale entries for the +// daemon's whole lifetime). The reduced agent_status frame omits `interfaces` +// entirely — guarded by the caller — and must not wipe what we know. +function router_learn(ifaces) { + if (type(ifaces) != "object") + return; + let macs = {}, ips = {}; + for (let ifname in ifaces) { + let i = ifaces[ifname]; + if (length(i?.mac ?? "")) + macs[lc(i.mac)] = ifname; + for (let a in i?.addr ?? []) + ips[a] = ifname; + } + router.macs = macs; + router.ips = ips; +} + +// netifyd publishes the same interface block in its status file; read it once +// at start-up so attribution is correct before the first agent_status arrives. +function router_bootstrap() { + let raw; + try { + raw = fs.readfile(STATUS_FILE); + } catch (e) { + return; + } + if (!raw) + return; + try { + router_learn(json(raw).interfaces); + } catch (e) { + // status.json is rewritten in place by netifyd; a torn read is normal + } +} + +function is_router(mac, ip) { + return (length(mac ?? "") && router.macs[mac] != null) || + (length(ip ?? "") && router.ips[ip] != null); +} + +// IEEE 802 group addresses: IPv4 multicast (01:00:5e:*), IPv6 multicast +// (33:33:*) and broadcast. These are not devices — attributing them per +// address would let any LAN host mint unbounded device aggregates by looping +// over multicast groups, so they collapse into one pseudo-device. +function is_group_mac(mac) { + if (!length(mac ?? "")) + return false; + return substr(mac, 0, 8) == "01:00:5e" || + substr(mac, 0, 5) == "33:33" || + mac == "ff:ff:ff:ff:ff:ff"; +} + +/* ------------------------------------------------------------------ * + * Flow table (§3.2, §3.3) + * ------------------------------------------------------------------ */ + +function flow_detach(fr) { + if (fr.shadow) + return; + if (fr.a && fr.a.cur.flows > 0) fr.a.cur.flows--; + if (fr.d && fr.d.cur.flows > 0) fr.d.cur.flows--; + if (fr.c && fr.c.cur.flows > 0) fr.c.cur.flows--; +} + +function flow_attach(fr) { + if (fr.shadow) + return; + if (fr.a) fr.a.cur.flows++; + if (fr.d) fr.d.cur.flows++; + if (fr.c) fr.c.cur.flows++; +} + +function flow_drop(digest) { + let fr = flows[digest]; + if (!fr) + return; + flow_detach(fr); + delete flows[digest]; + nflows--; +} + +// Identify (or re-identify, on a detection update) an existing flow record. +/* ------------------------------------------------------------------ * + * NAT twin resolution (3.2) + * ------------------------------------------------------------------ */ + +// On the EXTERNAL (WAN) capture of a NAT-ed flow, netifyd 4.4.7 reports the +// POST-translation identity: local_ip and local_mac are this router's own WAN +// address and MAC. That is indistinguishable from a flow the router itself +// originated, so the capture cannot be classified from the event alone. +// +// netifyd's own ip_nat field is exactly the question being asked, and it is +// carried on the record as fr.nat, but 4.4.7 does not set it on this path: +// measured 0 true out of 24 external captures of genuinely NAT-ed client +// flows, and 0 out of 24 on the matching internal captures. It is therefore +// read but never trusted as evidence. +// +// conntrack does know, because it holds both halves of the translation. For a +// masqueraded flow the ORIGINAL source is the LAN client while the REPLY +// destination is this router's WAN address; for router-originated traffic the +// two are the same address: +// +// src=192.168.1.50 dst=P ... src=P dst=192.168.72.10 <- client, NAT-ed +// src=192.168.72.10 dst=Q ... src=Q dst=192.168.72.10 <- router's own +// +// Reading a snapshot is not free, so it is reused for CT_TTL_MS: a burst of new +// flows shares one read, and an idle router never reads at all, because loading +// is demand-driven from ct_nat_twin() below. +function ct_load(now, force) { + if (conntrack.avail == false && (now - conntrack.probed) < CT_RETRY_MS) + return; + // A forced reload comes from a lookup miss, which is the ordinary case for a + // flow younger than the current snapshot. Honour it, but never more often + // than CT_FORCE_MIN_MS, so a burst of new flows cannot turn into a burst of + // reads of a file that is large on a busy router. + if (force) { + if ((now - conntrack.at) < CT_FORCE_MIN_MS) + return; + } else if (conntrack.at && (now - conntrack.at) < CT_TTL_MS) { + // Throttle on "we read recently", NOT on "we read recently and got + // entries". Gating this on conntrack.entries meant a snapshot that + // parsed to zero rows while the file was readable disabled the throttle + // entirely, and the unforced path has no CT_FORCE_MIN_MS floor, so every + // external flow identification re-read the whole file. + return; + } + conntrack.at = now; + conntrack.probed = now; + + // Read INCREMENTALLY, one line at a time. This used to be fs.readfile() + // followed by split(raw, "\n"), which materialised the entire conntrack + // table and then the entire line array before CT_MAX was ever consulted -- + // so CT_MAX bounded the retained map but not peak allocation, and the + // comment on the constant calling it a memory bound was wrong. On a busy + // router that is megabytes of transient garbage per read, on a device that + // may have 64 MB. Reading a line at a time makes the peak one line, and + // the CT_MAX break now stops the work as well as the retention. + let fd; + try { + fd = fs.open(cfg.conntrack_path ?? CONNTRACK_FILE, "r"); + } catch (e) { + fd = null; + } + if (!fd) { + if (conntrack.avail != false) { + conntrack.avail = false; + conntrack.map = {}; + conntrack.entries = 0; + logonce("ct-missing", "warn", + "%s unreadable: cannot distinguish a NAT twin from router-originated traffic, so external captures are shadowed while an internal capture runs. Client accounting stays correct; the router's own traffic is not attributed.", + cfg.conntrack_path ?? CONNTRACK_FILE); + } + return; + } + conntrack.avail = true; + + let map = {}, n = 0; + while (true) { + if (n >= CT_MAX) + break; + let line = fd.read("line"); + if (line == null || !length(line)) + break; + // Each line carries two tuples in order, original then reply. Collect the + // fields positionally rather than matching the whole line: the optional + // state word (TIME_WAIT, [UNREPLIED], ...) shifts the columns and differs + // between protocols. + let src = [], dst = [], sport = [], dport = []; + for (let tok in split(line, " ")) { + if (substr(tok, 0, 4) == "src=") push(src, substr(tok, 4)); + else if (substr(tok, 0, 4) == "dst=") push(dst, substr(tok, 4)); + else if (substr(tok, 0, 6) == "sport=") push(sport, substr(tok, 6)); + else if (substr(tok, 0, 6) == "dport=") push(dport, substr(tok, 6)); + } + if (length(src) < 2 || length(dst) < 2 || length(sport) < 2 || length(dport) < 2) + continue; + // Key on the reply tuple; value is whether the source was translated. + map[src[1] + "|" + dst[1] + "|" + sport[1] + "|" + dport[1]] = (src[0] != dst[1]); + n++; + } + try { fd.close(); } catch (e) {} + conntrack.map = map; + conntrack.entries = n; +} + +// true = this external capture is the WAN-side twin of a NAT-ed LAN flow +// false = conntrack positively says the router originated it +// null = conntrack cannot say (absent, unreadable, or the entry has expired) +function ct_nat_twin(f, now, fr) { + // THE PORTS COME FROM THE FLOW RECORD, NOT THE EVENT, AND THAT IS THE WHOLE + // FIX. netifyd puts local_port/other_port on the initial `flow` event and + // on nothing afterwards: a `flow_stats` payload carries addresses and byte + // counters only. This key was built straight off the event, so every call + // made from an update produced "...|93.184.216.34|192.168.72.10||" with two + // empty fields, matched nothing, and returned null. + // + // Nothing detected that. reshadow() treats null as grounds to discard, so + // EVERY router-originated flow was shadowed on its first stats update, and + // the "Router" device counted zero bytes on every install since 1.0.x. The + // dashboard then correctly hides a zero-byte device, so the feature was + // absent rather than visibly broken. Production had logged 107,476 + // positive identifications against a row reading zero for 2.6 days. + // + // Found 2026-08-31 by instrumenting the daemon after the row was noticed + // missing on the bench. See tests/protocol-suite.sh `router`. + let lp = (fr != null && fr.local_port != null) ? fr.local_port : f.local_port; + let op = (fr != null && fr.other_port != null) ? fr.other_port : f.other_port; + let key = (f.other_ip ?? "") + "|" + (f.local_ip ?? "") + "|" + + (op ?? "") + "|" + (lp ?? ""); + ct_load(now, false); + let v = conntrack.entries ? conntrack.map[key] : null; + // A miss usually means the flow is newer than the snapshot, because netifyd + // identifies a flow within moments of it starting. Refresh once and retry + // before giving up: without this the common case resolves to "unknown" and + // every router-originated flow is shadowed, which silently degrades to the + // no-conntrack behaviour even though conntrack is available. + if (v == null && conntrack.avail != false) { + ct_load(now, true); + v = conntrack.entries ? conntrack.map[key] : null; + } + return (v == null) ? null : v; +} + +/* ------------------------------------------------------------------ * + * AI service breakout (§10) + * + * WHY THIS EXISTS, measured rather than assumed. + * + * netifyd cannot classify AI traffic and will not soon. Its signature set, + * /etc/netify.d/netify-apps.conf, is dated 10 August 2023 on both routers + * checked, carries 199 application signatures, and contains ZERO entries for + * anthropic, claude, openai, chatgpt, huggingface, perplexity, mistral, + * copilot, bard or gemini. So this traffic arrives with detected_application 0 + * and lands in generic HTTP/S alongside everything else. + * + * But the identifying data is already here. netifyd reports the TLS SNI as + * host_server_name, and flows to the real endpoints carry api.anthropic.com, + * claude.ai and api.openai.com with no code change on our side. The daemon is + * therefore the right layer: aggregation happens here, and the browser only + * ever sees summarised rows. + * + * THIS IS HOSTNAME MATCHING, NOT DPI, AND MUST BE DESCRIBED THAT WAY. It + * asserts a classification netifyd did not make. The technique is legitimate + * -- matching the SNI is the same mechanism netifyd itself uses for HTTP/S -- + * but the honest framing is "we recognise the name", not "we inspected the + * protocol". + * + * Three limitations, stated here so they are not discovered later: + * + * - The list rots. New services appear constantly and nothing here updates + * itself. `ai_breakout '0'` turns the whole thing off. + * - ECH (Encrypted Client Hello) encrypts the SNI and would end this + * outright. netifyd has the identical exposure, so this is not worse than + * the status quo, but it is not durable either. + * - Region-encoded hostnames under a generic cloud domain cannot be matched + * by suffix without false positives. bedrock-runtime.us-east-1.amazonaws.com + * is the obvious example and is deliberately absent: no suffix of it is + * specific to Bedrock, and matching amazonaws.com would label every S3 + * bucket on the network as AI. + * + * ACCOUNTING IS NOT AFFECTED. This changes an identity, never a byte count. + * account() never sees it, so byte conservation is exactly as it was and the + * existing conservation checks still hold. + */ + +/* Keyed on a domain suffix, matched against the last two, three or four labels + * of the SNI. Value is [ app label, category, strong ]. + * + * THE CATEGORY IS A SLUG, NOT A DISPLAY STRING, and it must stay one. Until + * 1.1.1 this column held "AI assistants" and its three siblings, which read + * correctly in English and could therefore never be translated: the browser + * renders a category by looking the slug up in a table of `_()` literals, and + * a string that arrives already rendered has no key to look up. Every netifyd + * category reaches the frontend as a lowercase slug; these four were the only + * ones that did not, and they were ours. See catLabel() in common.js, which + * carries the matching `_('AI assistants')`. + * + * `strong` means: override even when netifyd DID identify an application. It + * is set only where netifyd is known to recognise the PARENT brand and would + * otherwise swallow the AI service inside it -- Gemini reported as "Google", + * Copilot as "GitHub". Everywhere else the rule is conservative: netifyd's own + * answer wins, so the day Netify ships AI signatures this table goes quiet by + * itself instead of permanently shadowing better data. + */ +const AI_HOSTS = { + // ---- assistants and model vendors -------------------------------- + "anthropic.com": [ "Anthropic (Claude)", "ai-assistants", false ], + "claude.ai": [ "Anthropic (Claude)", "ai-assistants", false ], + "openai.com": [ "OpenAI", "ai-assistants", false ], + "chatgpt.com": [ "OpenAI (ChatGPT)", "ai-assistants", false ], + "oaistatic.com": [ "OpenAI", "ai-assistants", false ], + "oaiusercontent.com": [ "OpenAI", "ai-assistants", false ], + "gemini.google.com": [ "Google Gemini", "ai-assistants", true ], + "aistudio.google.com": [ "Google Gemini", "ai-assistants", true ], + "generativelanguage.googleapis.com": + [ "Google Gemini", "ai-assistants", true ], + "x.ai": [ "xAI (Grok)", "ai-assistants", false ], + "grok.com": [ "xAI (Grok)", "ai-assistants", false ], + "mistral.ai": [ "Mistral", "ai-assistants", false ], + "cohere.com": [ "Cohere", "ai-assistants", false ], + "cohere.ai": [ "Cohere", "ai-assistants", false ], + "deepseek.com": [ "DeepSeek", "ai-assistants", false ], + "perplexity.ai": [ "Perplexity", "ai-assistants", false ], + "moonshot.cn": [ "Moonshot (Kimi)", "ai-assistants", false ], + "bigmodel.cn": [ "Zhipu", "ai-assistants", false ], + "dashscope.aliyuncs.com": + [ "Alibaba Qwen", "ai-assistants", true ], + "ai21.com": [ "AI21", "ai-assistants", false ], + "meta.ai": [ "Meta AI", "ai-assistants", false ], + "reka.ai": [ "Reka", "ai-assistants", false ], + "character.ai": [ "Character.AI", "ai-assistants", false ], + "poe.com": [ "Poe", "ai-assistants", false ], + "phind.com": [ "Phind", "ai-assistants", false ], + + // ---- media generation -------------------------------------------- + "midjourney.com": [ "Midjourney", "ai-media", false ], + "stability.ai": [ "Stability AI", "ai-media", false ], + "runwayml.com": [ "Runway", "ai-media", false ], + "elevenlabs.io": [ "ElevenLabs", "ai-media", false ], + "suno.com": [ "Suno", "ai-media", false ], + "suno.ai": [ "Suno", "ai-media", false ], + "udio.com": [ "Udio", "ai-media", false ], + "lumalabs.ai": [ "Luma", "ai-media", false ], + "bfl.ai": [ "Black Forest Labs", "ai-media", false ], + "ideogram.ai": [ "Ideogram", "ai-media", false ], + "leonardo.ai": [ "Leonardo", "ai-media", false ], + "pika.art": [ "Pika", "ai-media", false ], + "heygen.com": [ "HeyGen", "ai-media", false ], + "synthesia.io": [ "Synthesia", "ai-media", false ], + "descript.com": [ "Descript", "ai-media", false ], + + // ---- coding assistants and dev tools ----------------------------- + "githubcopilot.com": [ "GitHub Copilot", "ai-developer", true ], + "copilot.microsoft.com":[ "Microsoft Copilot", "ai-developer", true ], + "cursor.sh": [ "Cursor", "ai-developer", false ], + "cursor.com": [ "Cursor", "ai-developer", false ], + "codeium.com": [ "Codeium", "ai-developer", false ], + "windsurf.com": [ "Windsurf", "ai-developer", false ], + "tabnine.com": [ "Tabnine", "ai-developer", false ], + "sourcegraph.com": [ "Sourcegraph Cody", "ai-developer", false ], + "v0.dev": [ "v0", "ai-developer", false ], + "bolt.new": [ "Bolt", "ai-developer", false ], + "lovable.dev": [ "Lovable", "ai-developer", false ], + "replit.com": [ "Replit", "ai-developer", false ], + + // ---- aggregators, inference APIs, GPU rental, AI infrastructure --- + "openrouter.ai": [ "OpenRouter", "ai-infrastructure", false ], + "together.ai": [ "Together AI", "ai-infrastructure", false ], + "together.xyz": [ "Together AI", "ai-infrastructure", false ], + "replicate.com": [ "Replicate", "ai-infrastructure", false ], + "replicate.delivery": [ "Replicate", "ai-infrastructure", false ], + "fireworks.ai": [ "Fireworks AI", "ai-infrastructure", false ], + "groq.com": [ "Groq", "ai-infrastructure", false ], + "deepinfra.com": [ "DeepInfra", "ai-infrastructure", false ], + "novita.ai": [ "Novita", "ai-infrastructure", false ], + "hyperbolic.xyz": [ "Hyperbolic", "ai-infrastructure", false ], + "anyscale.com": [ "Anyscale", "ai-infrastructure", false ], + "sambanova.ai": [ "SambaNova", "ai-infrastructure", false ], + "cerebras.ai": [ "Cerebras", "ai-infrastructure", false ], + "huggingface.co": [ "Hugging Face", "ai-infrastructure", false ], + "hf.co": [ "Hugging Face", "ai-infrastructure", false ], + "vast.ai": [ "Vast.ai", "ai-infrastructure", false ], + "runpod.io": [ "RunPod", "ai-infrastructure", false ], + "runpod.ai": [ "RunPod", "ai-infrastructure", false ], + "lambdalabs.com": [ "Lambda", "ai-infrastructure", false ], + "paperspace.com": [ "Paperspace", "ai-infrastructure", false ], + "coreweave.com": [ "CoreWeave", "ai-infrastructure", false ], + "modal.com": [ "Modal", "ai-infrastructure", false ], + "baseten.co": [ "Baseten", "ai-infrastructure", false ], + "beam.cloud": [ "Beam", "ai-infrastructure", false ], + "salad.com": [ "Salad", "ai-infrastructure", false ], + "tensordock.com": [ "TensorDock", "ai-infrastructure", false ], + "fly.io": [ "Fly.io", "ai-infrastructure", false ], + "wandb.ai": [ "Weights & Biases", "ai-infrastructure", false ], + "langchain.com": [ "LangChain", "ai-infrastructure", false ], + "pinecone.io": [ "Pinecone", "ai-infrastructure", false ], + "weaviate.io": [ "Weaviate", "ai-infrastructure", false ], + "qdrant.tech": [ "Qdrant", "ai-infrastructure", false ], + "trychroma.com": [ "Chroma", "ai-infrastructure", false ] +}; + +/* Match the SNI against AI_HOSTS by domain suffix. + * + * SUFFIX, ANCHORED ON A LABEL BOUNDARY -- never a substring test. A substring + * match on "anthropic.com" also matches "anthropic.com.attacker.example", which + * is a hostname anyone can register and point at anything. Splitting on labels + * and rebuilding the last N makes that impossible by construction rather than + * by care. + * + * Tries the last two labels first, then three, then four, so a specific entry + * (gemini.google.com) is reachable without a linear scan of the table and a + * generic parent cannot shadow it. + */ +function ai_match(host) { + let h = lc(trim(host ?? "")); + + if (!length(h)) + return null; + + /* A trailing dot is legal in a FQDN and would otherwise produce an empty + * final label, shifting every suffix by one and matching nothing. */ + if (substr(h, -1) == ".") + h = substr(h, 0, length(h) - 1); + + let parts = split(h, "."); + let n = length(parts); + + if (n < 2) + return null; + + for (let take = 2; take <= 4 && take <= n; take++) { + let suffix = join(".", slice(parts, n - take)); + + /* User entries first, so `config hostmap` can override a built-in that + * is wrong as well as add one that is missing. */ + let hit = (cfg?.hostmap ?? {})[suffix] ?? AI_HOSTS[suffix]; + + /* Type-checked rather than merely truthy. + * + * In a BROWSER this would be load-bearing: `{}["constructor"]` returns + * a function, so a hostname of "constructor" would read as a hit. In + * ucode it is not, because ucode objects have no prototype chain and + * that lookup is null. Verified by sabotage: replacing this with a + * truthiness test changes no test result. + * + * It stays because what it actually guards is a malformed entry in the + * table above -- a two-element row, or a string where an array belongs + * -- which would otherwise be indexed as if it were well formed and + * produce a null label on a live router. It is NOT a defence against a + * prototype-key hostname, and should not be described as one. */ + if (type(hit) == "array" && length(hit) == 3) + return { app: hit[0], cat: hit[1], strong: hit[2], via: suffix }; + } + + return null; +} + +function flow_identify(fr, ev, f, now) { + let app_id = num(f.detected_application, 0); + let proto_id = num(f.detected_protocol, 0); + + // Cap any tag taken straight off the wire (detected_*_name) so a single + // flow record cannot hold an unbounded string. + let cap = (s) => (length(s ?? "") > TAG_MAX) ? substr(s, 0, TAG_MAX) : s; + + let key, label; + if (app_id > 0) { + key = "a" + app_id; + fr.tag = cap(f.detected_application_name ?? names.app[app_id]); + label = display_name(fr.tag); + } else if (proto_id > 0) { + key = "p" + proto_id; + fr.tag = cap(f.detected_protocol_name ?? names.proto[proto_id]); + label = display_name(fr.tag); + } else { + key = "unknown"; + label = "Unknown"; + fr.tag = "unknown"; + } + if (!length(label ?? "")) { + label = display_name(f.detected_protocol_name ?? names.proto[proto_id]) ?? "Unknown"; + if (!length(fr.tag ?? "")) + fr.tag = "unknown"; + } + + let cat = catnames.app[num(f.category?.application, -1)] ?? + catnames.proto[num(f.category?.protocol, -1)] ?? "unclassified"; + + if (length(f.host_server_name ?? "")) + fr.host = substr(f.host_server_name, 0, 63); + + // AI service breakout (10). Runs AFTER the netifyd identity is resolved and + // after `cat`, because it replaces both, and BEFORE attribution, because + // nothing below this point depends on the app identity. + // + // The gate is deliberately conservative: netifyd's own answer wins unless + // the entry is marked `strong`, which is set only where netifyd is known to + // recognise the parent brand and would swallow the AI service inside it. + // So the day Netify ships AI signatures, this table falls silent by itself + // rather than permanently shadowing better data. + // + // Identity only. No byte count is touched here, account() never sees this, + // and conservation is unaffected. + if (cfg.ai_breakout && length(fr.host ?? "")) { + let ai = ai_match(fr.host); + + if (ai && (app_id == 0 || ai.strong)) { + // Key on the LABEL, not on the matched suffix: anthropic.com and + // claude.ai are one vendor and must aggregate into one row, not two + // identically-named ones. + let slug = lc(replace(ai.app, /[^a-zA-Z0-9]+/g, "-")); + slug = replace(slug, /^-+/, ""); + slug = replace(slug, /-+$/, ""); + + key = "ai:" + slug; + label = ai.app; + // The icon pack is keyed on this tag. An `appflow.` prefix keeps the + // namespace honest: nothing here pretends to have come from netifyd. + fr.tag = "appflow." + slug; + cat = ai.cat; + stats.ai_labeled++; + } + } + + // Attribution (§3.2). netifyd's "local"/"other" labels follow its own + // topology view, not ours, so decide which endpoint is *our* device: + // - on an internal (LAN bridge) capture the device is the non-router side; + // - on an external (WAN) capture our own box is the device and the peer is + // upstream infrastructure, so the device is the router-owned side. + // Ports only ever arrive on the initial `flow` event. Keep them on the + // record so conntrack can still be asked about this flow later; see + // ct_nat_twin(). + if (f.local_port != null) fr.local_port = f.local_port; + if (f.other_port != null) fr.other_port = f.other_port; + + let lm = lc(f.local_mac ?? ""), om = lc(f.other_mac ?? ""); + let rl = is_router(lm, f.local_ip), ro = is_router(om, f.other_ip); + let dev_local = ev.internal ? !(rl && !ro) : !(ro && !rl); + let dev_is_router = dev_local ? rl : ro; + let mac = dev_local ? lm : om; + let ip = dev_local ? f.local_ip : f.other_ip; + // Honour netifyd's other_type when it names a group endpoint. netifyd's + // enum documents "broadcast"/"multicast", though only "local"/"remote" were + // seen on this bench, so the group-bit MAC test below is the path that + // actually fires today; the other_type check is a forward-compatible + // preference that costs nothing and needs no re-verification to be safe (it + // only ever adds a positive on a documented value). + let ot = f.other_type; + let group = (ot == "multicast" || ot == "broadcast") || + is_group_mac(mac) || is_group_mac(dev_local ? om : lm); + let dkey = group ? "multicast" + : (dev_is_router ? "router" : (length(mac) ? mac : (ip ?? "unknown"))); + + // Single-count each flow. The SHIPPING rule is documented in DESIGN §3.2 + // (which this implements; the earlier ip_nat-pairing design was replaced): + // with netifyd watching the LAN bridge (internal) and WAN (external) at once, + // a client's traffic is captured twice. Rather than pair the two captures by + // a fragile remote-endpoint key, count deterministically -- an internal + // capture is always the device's own copy, while an external capture is + // counted only when the device is the router itself (router-originated + // traffic never crosses the bridge); an external client-flow capture is + // therefore a NAT twin and is shadowed. + // + // NOT order-independent in the strict sense: saw_internal is a process-wide + // capability latch ("is any internal capture running"), not per-flow evidence + // of a specific twin. A WAN-side flow identified BEFORE the first internal + // event is counted; the same flow after is shadowed. Left there, a + // pre-existing external stub identified while saw_internal was still false + // would stay counted alongside its internal twin for its whole life (a real + // double-count of the central total, not a brief startup blip) -- reshadow() + // re-applies this rule on later stats updates to close that window. The + // per-flow twin question, and the exact contents of local_mac/other_mac when + // ip_nat is set, remain the open bench-test item in DESIGN §3.2 / §8. + if (fr.internal) + saw_internal = true; + + // Single-count decision for an EXTERNAL capture, once an internal capture is + // known to be running. Two cases, and they are not symmetric: + // + // device is not the router -> a LAN client seen on the WAN side, which only + // happens as the twin of its own internal + // capture. Shadow it. + // device IS the router -> ambiguous by construction, because netifyd + // reports the post-NAT identity. Ask conntrack. + // + // When conntrack cannot answer, shadow. The two possible errors are not + // equal: counting a twin double-counts a client's entire traffic, measured at + // 163-199% of wire truth, while shadowing router-originated traffic omits the + // router's own DNS, NTP and updates. Default to the smaller error. + let want_shadow = false; + if (saw_internal && !fr.internal && !group) { + if (!dev_is_router) + want_shadow = true; + else { + let twin = ct_nat_twin(f, now, fr); + want_shadow = (twin != false); + if (twin == false) + fr.ct_local = true; // a positive finding, remembered + if (twin == null) + stats.ct_unresolved++; + else if (twin) + stats.ct_twins++; + else + stats.ct_local++; + } + } + + if (fr.app_key == key && fr.cat_key == cat && fr.dev_key == dkey && + fr.shadow == want_shadow) { + if (!fr.dev_ip) + fr.dev_ip = ip; + return; // nothing structural changed + } + + // Re-pointing an already-counted flow: bytes already attributed stay where + // they were (the accounting seam is append-only by design); only future + // bytes and the active-flow counts follow the new identity. + flow_detach(fr); // uses the OLD shadow state + if (want_shadow && !fr.shadow) + stats.shadowed++; + fr.shadow = want_shadow; + if (fr.shadow) { // a shadowed twin tracks nothing + fr.app_key = key; fr.cat_key = cat; fr.dev_key = dkey; + fr.dev_mac = length(mac) ? mac : null; fr.dev_ip = ip; + fr.dev_local = dev_local; + fr.a = null; fr.d = null; fr.c = null; + return; + } + fr.app_key = key; + fr.app_name = label ?? "Unknown"; + fr.cat_key = cat; + fr.dev_key = dkey; + fr.dev_mac = length(mac) ? mac : null; + fr.dev_ip = ip; + fr.dev_local = dev_local; + fr.a = agg_make(apps, "app", key, fr.app_name, now); + fr.d = agg_make(devs, "device", dkey, device_name(dkey, fr.dev_mac, ip, now), now); + fr.c = agg_make(cats, "category", cat, cat, now); + if (fr.d) { + fr.d.mac = fr.dev_mac; + // Keep the friendliest address for display: an interface usually has + // both, and an IPv4 address means more to a user than a link-local v6. + if (ip && (!fr.d.ip || (index(ip, ".") >= 0 && index(fr.d.ip, ".") < 0))) + fr.d.ip = ip; + fr.d.router = (dkey == "router"); + } + if (fr.a) { + fr.a.tag = fr.tag; + fr.a.cat = cat; + } + flow_attach(fr); +} + +// Re-apply the single-count rule to a flow whose shadow decision was frozen at +// creation. flow_identify runs once per flow (at flow_new) and again only on a +// `flow` event; a pre-existing external stub, created from flow_stats before any +// internal event, is therefore identified with saw_internal still false, counted, +// and never revisited -- so it double-counts against its internal twin for its +// whole life. Once saw_internal latches, such a flow is an external non-router +// capture like any other and the shipping rule already says to shadow it; this +// just enforces that rule for flows that predated the latch, closing the window +// from "entire flow life" to "at most one stats interval". It only ever tightens +// (unshadowed -> shadowed), never resurrects. The residual sign question for an +// unidentifiable stub (a genuine NAT twin vs. pre-existing router-origin traffic, +// both landing in the "unknown" bucket) is the same §3.2 bench-test item. +function reshadow(fr, f, now) { + if (fr.shadow || !saw_internal || fr.internal) + return; + if (fr.dev_key == "multicast") + return; + if (fr.dev_key == "router") { + // A WAN-side capture of a NAT-ed client flow is classified "router", + // because netifyd reports the post-translation identity (§3.2). That is + // precisely the class this function exists to correct, so the blanket + // `return` that stood here until 2026-08-24 exempted the only flows that + // needed it: after a restart, an established flow's external copy stayed + // counted alongside its internal twin for the flow's whole remaining + // life, and stats.shadowed never moved because there was no transition. + // Ask conntrack, exactly as flow_identify does, and default the same way. + let twin = ct_nat_twin(f, now, fr); + if (twin == false) { + stats.ct_local++; + return; // conntrack says the router originated it + } + if (twin == null) { + stats.ct_unresolved++; + // ABSENT EVIDENCE IS NOT POSITIVE EVIDENCE, the same rule + // statusStrip() carries a comment about. If conntrack already told + // us this flow was router-originated, a later lookup that cannot + // answer -- because the connection closed and the entry aged out -- + // is not a reason to throw the flow away. Only a positive "this is + // a NAT twin" below may overturn it. + if (fr.ct_local) + return; + } else { + stats.ct_twins++; + } + } + flow_detach(fr); + fr.shadow = true; + fr.a = null; fr.d = null; fr.c = null; + stats.shadowed++; +} + +function flow_new(digest, ev, f, now, late) { + if (nflows >= cfg.flow_max) { + prune(now, true); + if (nflows >= cfg.flow_max) { + stats.flows_dropped++; + return null; + } + } + + let fr = { + digest, + iface: ev.interface ?? "?", + internal: (ev.internal == true), + nat: (f.ip_nat == true), + proto: num(f.ip_protocol, 0), + host: null, tag: null, + app_key: null, app_name: "Unknown", cat_key: "unclassified", + dev_key: "unknown", dev_mac: null, dev_ip: null, dev_local: true, + a: null, d: null, c: null, + up: 0, down: 0, total: 0, + start: num(f.first_seen_at, now), seen: now, + shadow: false, late: (late == true) + }; + flows[digest] = fr; + nflows++; + stats.flows_seen++; + if (late) + stats.flows_late++; + + // Attribution and single-count shadowing are both decided in flow_identify, + // deterministically from this flow alone (no cross-flow pairing state). + flow_identify(fr, ev, f, now); + return fr; +} + +/* ------------------------------------------------------------------ * + * Event handlers. Dispatch is table-driven: adding or renaming an event + * type is a one-line change. + * ------------------------------------------------------------------ */ + +function ev_hello(ev) { + agent.version = ev.agent_version ?? agent.version; + logmsg("info", "netify agent %s connected (%s)", + agent.version ?? "?", ev.build_version ?? ""); +} + +// In normal operation `names` is bounded by nDPI's catalogue (a few thousand +// ids from the definitions event). These caps make that bound explicit rather +// than incidental, so a buggy or hostile agent cannot grow the maps without +// limit: entries beyond NAME_MAX are dropped and every stored tag is truncated. +function store_name(tbl, id, tag) { + if (tbl[id] == null && length(tbl) >= NAME_MAX) + return; + tbl[id] = (length(tag ?? "") > TAG_MAX) ? substr(tag, 0, TAG_MAX) : tag; +} + +function ev_definitions(ev) { + let na = 0, np = 0; + for (let e in ev.applications ?? []) { + if (e?.id == null) continue; + store_name(names.app, e.id, e.tag); + na++; + } + for (let e in ev.protocols ?? []) { + if (e?.id == null) continue; + store_name(names.proto, e.id, e.tag); + np++; + } + logmsg("info", "definitions: %d applications, %d protocols", na, np); +} + +function ev_agent_status(ev) { + agent.version = ev.agent_version ?? agent.version; + agent.uptime = num(ev.uptime, agent.uptime); + agent.flow_count = num(ev.flow_count, 0); + agent.rss_kb = num(ev.maxrss_kb, 0); + agent.ts = num(ev.timestamp, 0) * 1000; + router_learn(ev.interfaces); + // ev.devices is netifyd's MAC -> IP[] map; used only to fill in an address + // for devices whose flows we have not seen an IP for yet. + for (let mac in ev.devices ?? {}) { + let d = devs[lc(mac)]; + if (d && !d.ip) + d.ip = (ev.devices[mac] ?? [])[0]; + } +} + +function ev_flow(ev, f, now) { + let fr = flows[f.digest]; + if (!fr) { + flow_new(f.digest, ev, f, now, false); + return; + } + fr.late = false; + flow_identify(fr, ev, f, now); +} + +// local_bytes/other_bytes are *per-update deltas* (verified on 4.4.7: they +// return to zero in the final flow_purge while total_bytes, cumulative, does +// not). This shared helper accounts one such update, bounding the directional +// deltas by the growth of total_bytes so a self-inconsistent counter can never +// inflate the totals. Both flow_stats and flow_purge go through it, so the +// clamp guarantee in DESIGN §2.2 holds on every accounting path — including the +// purge path, whose payload shape is the one the interface doc could not +// capture live. +function flow_update(fr, f, now, final) { + let dl = num(f.local_bytes, 0), do_ = num(f.other_bytes, 0); + let tb = num(f.total_bytes, 0); + + // Byte conservation (§2.5). What netifyd says this flow grew by, recorded + // before any clamping, so `bytes_reported` can be compared against what + // actually reached an aggregate. A late stub's first update is skipped: + // its total_bytes is the flow's whole pre-adoption history, which this + // daemon deliberately does not attribute, and counting it would show as + // permanent false drift rather than as the connect-blindness it is. + // Byte conservation (§2.5). netifyd's own statement of what moved, recorded + // BEFORE any clamp or shadow decision, so `leaked` in status exposes bytes + // that were reported to us and never reached an aggregate. It has to mirror + // the basis attribution actually uses: where a baseline exists the + // cumulative growth is authoritative, otherwise the directional deltas are + // all there is. A late stub's pre-adoption history is excluded by + // construction, because it lives in total_bytes and never in the deltas. + if (tb > 0 && fr.total > 0) { + let grew = tb - fr.total; + stats.bytes_reported += (grew < 0) ? tb : grew; + } else { + stats.bytes_reported += (dl + do_); + } + + // Bound the deltas by total_bytes growth ONLY when total_bytes is usable + // (present and moving forward). A missing or reset counter must not scale + // the real directional deltas to zero (that would silently and permanently + // stop accounting a flow); in that case trust the deltas as given. + if (tb > 0 && fr.total > 0) { + let dt = tb - fr.total; + if (dt < 0) + dt = tb; // netifyd counter reset: treat tb as the delta + let sum = dl + do_; + if (sum > dt) { + // Directional deltas exceed the cumulative growth: clamp them down. + dl = (sum > 0) ? int(dl * dt / sum) : 0; + do_ = dt - dl; + } else if (sum == 0 && dt > 0) { + // The cumulative counter advanced but this update carries no + // directional bytes. This is the flow_purge case on 4.4.7: + // local_bytes/other_bytes are always 0 there while total_bytes is + // final, so the bytes since the last flow_stats would otherwise be + // lost (a few percent of every flow). Attribute dt in the same + // up/down proportion the flow has shown so far (account() maps + // local/other -> up/down by dev_local, so split in that frame), + // defaulting to download when the direction is not yet known. + let up_share = (fr.up + fr.down > 0) ? int(dt * fr.up / (fr.up + fr.down)) : 0; + let down_share = dt - up_share; + if (fr.up + fr.down == 0) + down_share = dt; // unknown: count as download + // local == up when dev_local, else local == down. + dl = fr.dev_local ? up_share : down_share; + do_ = fr.dev_local ? down_share : up_share; + } + } else if (final && tb > 0 && fr.total == 0 && !fr.late && (dl + do_) == 0) { + // PURGE-ONLY FLOW: a `flow` event created this record, no flow_stats ever + // arrived, and now the purge carries the final cumulative counter with + // both directional fields zero. The branch above cannot run because it + // requires fr.total > 0, so without this the flow's entire byte count + // would be silently discarded -- and stats.no_total would not move + // either, because tb > 0. + // + // MEASURED 2026-08-24 (EA8500, netifyd 4.4.7): this does NOT fire on + // this netifyd. Tapping the export socket for 170 s, 64 of 64 flows + // whose full lifecycle was observed carried a flow_stats before their + // purge, UDP DNS queries included. DESIGN §2.5 reached the same + // conclusion from byte totals; this reached it from the events. + // + // It is here because that behaviour is undocumented and the daemon had + // no defence if it ever changed. Late stubs are excluded: for those tb + // is pre-adoption history and injecting it would overcount. + stats.purge_only++; + dl = fr.dev_local ? 0 : tb; // account() maps local/other -> up/down + do_ = fr.dev_local ? tb : 0; // by dev_local; count it as download + stats.bytes_reported += tb; + } else if (tb <= 0) { + stats.no_total++; + } + if (tb > 0) + fr.total = tb; + + account(fr, dl, do_, now); +} + +function ev_flow_stats(ev, f, now) { + let fr = flows[f.digest]; + if (!fr) { + // Stats for a digest we hold no flow event for. Two distinct causes + // with DIFFERENT outcomes; both characterised on hardware 2026-08-22 + // (EA8500, netifyd 4.4.7, deconfounded LAN-local traffic): + // + // 1. A genuine stats-before-flow race on a NEW flow. The flow event + // lands shortly after and identity follows via flow_identify(). + // + // 2. A flow already established when we connected or reconnected. + // netifyd 4.4.7 has no established-flow dump, so it emits + // flow_stats for such a flow but NEVER a flow event: identity + // never arrives and the bytes stay under Unknown until the flow + // ends. Measured by restarting this daemon mid-transfer, which + // stranded 10.7 MB of a correctly-classified HTTP flow (a fresh + // socket client saw 5 flow_stats and 0 flow events for it). + // Bounded to the connect/reconnect window, self-heals as flows + // cycle, and not fixable here without netifyd support. + // + // Either way the bytes are PARKED as Unknown, never dropped: byte + // conservation through this path is verified (DESIGN 2.5, 8). + fr = flow_new(f.digest, ev, f, now, true); + if (!fr) + return; + } + reshadow(fr, f, now); // close the stale-shadow double-count + flow_update(fr, f, now, false); +} + +function ev_purge(ev, f, now) { + let fr = flows[f.digest]; + stats.purges++; + if (!fr) + return; + // The purge carries the flow's final counter delta: account it, clamped like + // any other update, before the record goes away. + fr.reason = ev.reason ?? "purged"; + flow_update(fr, f, now, true); // `final`: the purge-only rescue path + flow_drop(f.digest); +} + +const HANDLERS = { + agent_hello: ev_hello, + definitions: ev_definitions, + agent_status: ev_agent_status +}; + +const FLOW_HANDLERS = { + flow: ev_flow, + flow_stats: ev_flow_stats, + flow_purge: ev_purge +}; + +function dispatch(ev, now) { + let t = ev.type; + if (!length(t ?? "")) { + stats.malformed++; + return; + } + + stats.events++; + if (stats.by_type[t] != null) + stats.by_type[t]++; + else if (length(stats.by_type) < EVENT_TYPE_MAX) + stats.by_type[t] = 1; + stats.last_event_ms = now; + + let h = HANDLERS[t]; + if (h) { + h(ev); + return; + } + + let f = ev.flow; + if (type(f) == "object" && length(f.digest ?? "")) { + h = FLOW_HANDLERS[t]; + if (h) { + h(ev, f, now); + return; + } + // Unknown but flow-scoped: a purge-ish name finalises the flow, which + // keeps a future netifyd rename from leaking flow records. + if (match(t, PURGE_RE)) { + ev_purge(ev, f, now); + return; + } + } + + stats.unknown++; + if (stats.unknown_types[t] == null && length(stats.unknown_types) < UNKNOWN_TYPE_MAX) + stats.unknown_types[t] = 0; + if (stats.unknown_types[t] != null) + stats.unknown_types[t]++; +} + +/* ------------------------------------------------------------------ * + * Wire protocol (§2.1): newline-delimited JSON, each payload preceded by a + * {"length": N} header line. + * ------------------------------------------------------------------ */ + +function feed(line, now) { + if (!length(line)) + return; + + let ev; + try { + ev = json(line); + } catch (e) { + stats.malformed++; + logonce("badjson", "warn", "malformed event line (%d bytes): %s", length(line), e); + return; + } + + if (type(ev) != "object") { + stats.malformed++; + return; + } + + // Frame header: sole key "length". + if (ev.length != null && length(ev) == 1) { + stats.frames++; + return; + } + + dispatch(ev, now); +} + +function on_readable() { + let now = now_ms(); + + for (let i = 0; i < RX_DRAIN_MAX; i++) { + let data; + try { + data = sock.recv(RX_CHUNK, socket.MSG_DONTWAIT); + } catch (e) { + disconnect("recv error: " + e); + return; + } + + if (data == null) { + let err = socket.error(true); + if (err == 11 || err == 4) // EAGAIN / EINTR + return; + disconnect(sprintf("recv failed: %s", socket.error())); + return; + } + + if (length(data) == 0) { + disconnect("netifyd closed the connection"); + return; + } + + stats.rx_bytes += length(data); + rxbuf += data; + + if (length(rxbuf) > RX_MAX) { + // Never let a peer that stops sending newlines grow us without + // bound (§3.3). Drop back to a line boundary and carry on. + stats.resyncs++; + logonce("rxmax", "warn", "receive buffer overflow, resyncing"); + let cut = rindex(rxbuf, "\n"); + rxbuf = (cut >= 0) ? substr(rxbuf, cut + 1) : ""; + } + + let parts = split(rxbuf, "\n"); + rxbuf = parts[length(parts) - 1]; + for (let n = 0; n < length(parts) - 1; n++) + feed(trim(parts[n]), now); + + if (length(data) < RX_CHUNK) + return; + } +} + +/* ------------------------------------------------------------------ * + * Connection management: start before netifyd, survive its restart. + * ------------------------------------------------------------------ */ + +disconnect = function(why) { + if (sock_fd) { + try { + sock_fd.delete(); + } catch (e) { + try { sock_fd.cancel(); } catch (e2) {} + } + sock_fd = null; + } + if (sock) { + try { sock.close(); } catch (e) {} + sock = null; + } + rxbuf = ""; + if (connected) { + connected = false; + logmsg("warn", "disconnected: %s", why); + } + schedule_reconnect(); +}; + +schedule_reconnect = function() { + if (retry_timer) + return; + retry_timer = uloop.timer(backoff_ms, function() { + retry_timer = null; + connect(); + }); + backoff_ms = clampi(backoff_ms * 2, RECONNECT_MIN_MS, RECONNECT_MAX_MS); +}; + +connect = function() { + try { + sock = socket.connect(cfg.socket_path); + } catch (e) { + sock = null; + } + + if (!sock) { + stats.connect_fail++; + logonce("noconn", "warn", "waiting for netifyd on %s", cfg.socket_path); + schedule_reconnect(); + return; + } + + sock_fd = uloop.handle(sock, on_readable, uloop.ULOOP_READ); + // Count reconnects, not the first connect, so `socket.reconnects` reads as + // churn that actually happened. + if (ever_connected) + stats.reconnects++; + ever_connected = true; + connected = true; + backoff_ms = RECONNECT_MIN_MS; + // A new socket is a new netifyd lifetime. saw_internal is a capability + // latch about the CURRENT stream ("is internal capture active"), so it must + // not carry over: a netifyd restarted external-only would otherwise keep the + // stale `true` and shadow every client flow to zero. Cleared here so the new + // stream re-establishes it from its own events. + saw_internal = false; + router_bootstrap(); + logmsg("info", "connected to %s", cfg.socket_path); +}; + +/* ------------------------------------------------------------------ * + * Housekeeping (§3.3) + * ------------------------------------------------------------------ */ + +prune = function(now, force) { + let cutoff = now - cfg.idle_ms; + let dropped = 0; + + for (let d in keys(flows)) { + let fr = flows[d]; + if (fr && fr.seen < cutoff) { + flow_drop(d); + dropped++; + } + } + + // Cap pressure: shed the least recently active tenth of the table. + // + // Counted separately into flows_shed as well as into the pruned total, so + // "the flow table is too small for this network" is visible instead of + // hiding under ordinary idle housekeeping. See the note beside flows_shed. + if (force && nflows >= cfg.flow_max) { + let list = []; + for (let d in flows) + push(list, [ d, flows[d].seen ]); + sort(list, (a, b) => a[1] - b[1]); + let want = int(cfg.flow_max / 10) + 1; + for (let i = 0; i < want && i < length(list); i++) { + flow_drop(list[i][0]); + dropped++; + stats.flows_shed++; + } + } + + stats.flows_pruned += dropped; + + // Aggregates with no live flows that have been silent for longer than the + // trailing-hour window carry no data anyone can still query. + let agg_cutoff = now - (SERIES_SLOTS * SERIES_MS) - 60000; + for (let tbl in [ apps, devs, cats ]) + for (let k in keys(tbl)) { + let a = tbl[k]; + let last = a.cur.last ? a.cur.last : a.cur.first; + if (a.cur.flows == 0 && last < agg_cutoff) { + delete tbl[k]; + stats.aggs_pruned++; + } + } +}; + +function housekeeping() { + let now = now_ms(); + stats.sweeps++; + let slot = int(now / SERIES_MS), dslot = int(now / DEVSER_MS); + + prune(now, false); + + // Advance the trailing-hour rings and roll the rate windows. Rolling here + // (not only in agg_bump) means an aggregate that has gone idle still sees + // its rate decay to zero, and it keeps agg_rate a pure read. + agg_roll(totals, now); + ring_sweep(totals.buckets, slot, SERIES_SLOTS); + for (let tbl in [ apps, devs, cats ]) + for (let k in tbl) { + agg_roll(tbl[k], now); + ring_sweep(tbl[k].buckets, slot, SERIES_SLOTS); + for (let dk in tbl[k].devs ?? {}) + ring_sweep(tbl[k].devs[dk].ring, dslot, DEVSER_SLOTS); + for (let hk in tbl[k].hosts ?? {}) + ring_sweep(tbl[k].hosts[hk].ring, dslot, DEVSER_SLOTS); + } + + // §7.3 seam: v1 has no persistent buckets, so this hands over nothing. + store.flush(totals.buckets, slot); +} + +/* ------------------------------------------------------------------ * + * ubus surface (§5) + * ------------------------------------------------------------------ */ + +function rss_kb() { + let raw; + try { + raw = fs.readfile("/proc/self/status"); + } catch (e) { + return 0; + } + let m = raw ? match(raw, /VmRSS:[ \t]*([0-9]+)/) : null; + return m ? int(m[1]) : 0; +} + +function envelope(now) { + return { generated_at: now, agent_connected: connected }; +} + +function metrics(a, now) { + let r = agg_rate(a, now); + return { + bytes_up: a.cur.up, + bytes_down: a.cur.down, + bytes_total: a.cur.up + a.cur.down, + rate_up: r[0], + rate_down: r[1], + rate_total: r[0] + r[1], + flows: a.cur.flows, + first_seen: a.cur.first, + last_seen: a.cur.last ? a.cur.last : a.cur.first + }; +} + +function app_row(a, now) { + let e = metrics(a, now); + e.key = a.key; + e.name = a.name; + e.tag = a.tag ?? a.key; + e.category = a.cat ?? null; + return e; +} + +function dev_row(a, now) { + let e = metrics(a, now); + let pseudo = (a.key == "router" || a.key == "multicast" || a.key == "unknown"); + e.key = a.key; + e.name = device_name(a.key, a.mac, a.ip, now); + // The pseudo-devices collapse many endpoints; a single leftover MAC/IP + // under them would be misleading, so present them without one. + e.mac = pseudo ? null : a.mac; + e.ip = pseudo ? null : a.ip; + e.is_router = (a.router == true); + return e; +} + +function cat_row(a, now) { + let e = metrics(a, now); + e.key = a.key; + e.name = a.name; + return e; +} + +const SORTERS = { + rate: (a, b) => (b.rate_total - a.rate_total) || (b.bytes_total - a.bytes_total), + bytes: (a, b) => b.bytes_total - a.bytes_total, + up: (a, b) => b.bytes_up - a.bytes_up, + down: (a, b) => b.bytes_down - a.bytes_down, + flows: (a, b) => b.flows - a.flows, + last: (a, b) => b.last_seen - a.last_seen, + name: (a, b) => (a.name < b.name) ? -1 : ((a.name > b.name) ? 1 : 0) +}; + +function rows_of(tbl, rowfn, now, sortkey, limit) { + let out = []; + for (let k in tbl) + push(out, rowfn(tbl[k], now)); + sort(out, SORTERS[sortkey ?? "rate"] ?? SORTERS.rate); + return (limit > 0 && length(out) > limit) ? slice(out, 0, limit) : out; +} + +function find_app(want) { + if (!length(want ?? "")) + return null; + if (apps[want]) + return apps[want]; + let lw = lc(want); + for (let k in apps) { + let a = apps[k]; + if (lc(a.tag ?? "") == lw || lc(a.name) == lw) + return a; + } + return null; +} + +function m_summary(req) { + let now = now_ms(); + let n = clampi(num(req.args?.limit, cfg.top_n), 1, LIST_REPLY_MAX); + let r = envelope(now); + r.window = cfg.rate_window; + r.totals = metrics(totals, now); + r.totals.flows = nflows; + r.top_apps = rows_of(apps, app_row, now, "rate", n); + r.top_devices = rows_of(devs, dev_row, now, "rate", n); + r.top_categories = rows_of(cats, cat_row, now, "rate", n); + r.counts = { + apps: length(apps), + devices: length(devs), + categories: length(cats), + flows: nflows + }; + return r; +} + +/* Application totals restricted to one device. + * + * The mirror of devices_for_apps(): same bounded flow table, same join already + * present on every flow record, grouped the other way. No new aggregate. + * + * As with that function these are the bytes CURRENTLY TRACKED flows account + * for, not the application's lifetime totals, and the reply says so. + */ +function apps_for_device(dev, now) { + let acc = {}; + + for (let dg in flows) { + let fr = flows[dg]; + + if (!fr || fr.shadow || fr.dev_key != dev || !fr.app_key) + continue; + + let e = acc[fr.app_key]; + if (!e) { + e = { key: fr.app_key, cat: fr.cat_key, up: 0, down: 0, flows: 0 }; + acc[fr.app_key] = e; + } + + e.up += (fr.up ?? 0); + e.down += (fr.down ?? 0); + e.flows++; + } + + let out = []; + for (let k in acc) { + let e = acc[k]; + /* Names live on the aggregate, not on the flow, so look the app up. A + * flow whose app has already been pruned from the aggregate still gets + * a row, keyed rather than named, instead of vanishing. */ + let a = apps[k]; + + push(out, { + key: k, + name: a ? a.name : k, + tag: a ? (a.tag ?? k) : k, + category: e.cat, + upload: e.up, + download: e.down, + bytes_total: e.up + e.down, + flows: e.flows + }); + } + + sort(out, (x, y) => y.bytes_total - x.bytes_total); + + return out; +} + +function m_apps(req) { + let now = now_ms(); + let r = envelope(now); + r.window = cfg.rate_window; + let lim = clampi(num(req.args?.limit, LIST_REPLY_MAX), 1, LIST_REPLY_MAX); + let dev = req.args?.device; + + /* `device` scopes the answer to one device: what is that thing doing. */ + if (type(dev) == "string" && length(dev)) { + let rows = apps_for_device(dev, now); + + r.from_flows = true; + r.device = dev; + r.total = length(rows); + r.apps = (length(rows) > lim) ? slice(rows, 0, lim) : rows; + + return r; + } + + r.from_flows = false; + r.apps = rows_of(apps, app_row, now, req.args?.sort, lim); + r.total = length(apps); + return r; +} + +/* Device totals restricted to a set of application keys. + * + * Computed from the live flow table rather than from a new aggregate. Every + * flow already carries dev_key and app_key, so the join is there; the table is + * bounded by flow_max, so this is bounded too. No new memory. + * + * The numbers are what the CURRENTLY TRACKED flows account for, not the + * device's lifetime totals: a flow that has ended is no longer here. The reply + * carries from_flows so a caller cannot present it as cumulative history. + * + * Shadow flows are excluded for the same reason they are excluded everywhere + * else: they are the NAT-side duplicate and counting them would double. + */ +function devices_for_apps(keys, now) { + let want = {}; + for (let k in (keys ?? [])) + if (type(k) == "string" && length(k)) + want[k] = true; + + let acc = {}; + for (let dg in flows) { + let fr = flows[dg]; + + if (!fr || fr.shadow || !fr.dev_key || !want[fr.app_key]) + continue; + + let e = acc[fr.dev_key]; + if (!e) { + e = { key: fr.dev_key, mac: fr.dev_mac, ip: fr.dev_ip, + up: 0, down: 0, flows: 0 }; + acc[fr.dev_key] = e; + } + if (!e.ip && fr.dev_ip) + e.ip = fr.dev_ip; + + e.up += (fr.up ?? 0); + e.down += (fr.down ?? 0); + e.flows++; + } + + let out = []; + for (let k in acc) { + let e = acc[k]; + let pseudo = (k == "router" || k == "multicast" || k == "unknown"); + + push(out, { + key: k, + name: device_name(k, e.mac, e.ip, now), + mac: pseudo ? null : e.mac, + ip: pseudo ? null : e.ip, + upload: e.up, + download: e.down, + bytes_total: e.up + e.down, + flows: e.flows + }); + } + + sort(out, (x, y) => y.bytes_total - x.bytes_total); + + return out; +} + +function m_devices(req) { + let now = now_ms(); + let r = envelope(now); + r.window = cfg.rate_window; + let lim = clampi(num(req.args?.limit, LIST_REPLY_MAX), 1, LIST_REPLY_MAX); + + /* `apps` restricts the answer to the devices carrying those applications, + * which is how the UI answers "who is generating this traffic" once the + * user has filtered the application list. Matching stays in the UI, where + * the user's text lives; summing stays here, where the flows are. */ + let keys = req.args?.apps; + + if (type(keys) == "array" && length(keys)) { + let rows = devices_for_apps(keys, now); + + r.from_flows = true; + r.total = length(rows); + r.devices = (length(rows) > lim) ? slice(rows, 0, lim) : rows; + + return r; + } + + r.from_flows = false; + r.devices = rows_of(devs, dev_row, now, req.args?.sort, lim); + r.total = length(devs); + return r; +} + +function m_app_detail(req) { + let now = now_ms(); + let a = find_app(req.args?.app); + let r = envelope(now); + if (!a) { + r.found = false; + r.app = req.args?.app; + return r; + } + + let slot = int(now / SERIES_MS), dslot = int(now / DEVSER_MS); + r.found = true; + r.app = a.key; + r.tag = a.tag ?? a.key; + r.label = a.name; + // The detail drawer is opened from the Statistics table, which is a + // trailing-hour view. Report the byte totals over that same window (from the + // hour ring) so the drawer header cannot disagree with the row that opened it; + // the earlier metrics(a) returned session-lifetime bytes, which read as a + // gross mismatch under an "last 60 minutes" caption. Rate and flow context + // stay as-is (a rate is inherently current). + let at = ring_total(a.buckets, slot, SERIES_SLOTS); + r.metrics = metrics(a, now); + r.metrics.bytes_down = at[0]; + r.metrics.bytes_up = at[1]; + r.metrics.bytes_total = at[0] + at[1]; + r.metrics.window = "hour"; + r.time_series = ring_series(a.buckets, slot, SERIES_SLOTS, SERIES_MS); + + let ds = []; + for (let k in a.devs ?? {}) { + let e = a.devs[k]; + let t = ring_total(e.ring, dslot, DEVSER_SLOTS); + push(ds, { + key: k, + mac: e.mac, + ip: e.ip, + name: device_name(k, e.mac, e.ip, now), + download: t[0], + upload: t[1], + total: t[0] + t[1], + last_seen: e.last + }); + } + sort(ds, (x, y) => (y.total - x.total)); + r.devices = ds; + + let hs = []; + for (let h in a.hosts ?? {}) { + let ht = ring_total(a.hosts[h].ring, dslot, DEVSER_SLOTS); + push(hs, { + host: h, + download: ht[0], + upload: ht[1], + total: ht[0] + ht[1], + last_seen: a.hosts[h].last + }); + } + sort(hs, (x, y) => (y.total - x.total)); + r.hosts = hs; + + return r; +} + +function m_flows(req) { + let now = now_ms(); + let want_app = req.args?.app, want_dev = req.args?.device; + let limit = clampi(num(req.args?.limit, 50), 1, FLOWS_REPLY_MAX); + let out = []; + + for (let d in flows) { + let fr = flows[d]; + if (length(want_app ?? "") && fr.app_key != want_app && fr.tag != want_app) + continue; + if (length(want_dev ?? "") && fr.dev_key != want_dev) + continue; + push(out, { + digest: substr(d, 0, 16), + app: fr.app_key, + app_name: fr.app_name, + category: fr.cat_key, + device: fr.dev_key, + device_name: device_name(fr.dev_key, fr.dev_mac, fr.dev_ip, now), + host: fr.host, + interface: fr.iface, + internal: fr.internal, + nat: fr.nat, + shadow: fr.shadow, + ip_protocol: fr.proto, + upload: fr.up, + download: fr.down, + // netifyd's cumulative lifetime counter for the flow, NOT the sum of + // what appflow attributed (up+down): a stub flow adopted mid-life has + // a large lifetime but small attributed bytes. Named distinctly so a + // consumer never confuses it with the aggregate `bytes_total`. + lifetime_bytes: fr.total, + first_seen: fr.start, + last_seen: fr.seen + }); + } + + sort(out, (x, y) => y.last_seen - x.last_seen); + let r = envelope(now); + r.total = length(out); + r.truncated = (length(out) > limit); + r.flows = r.truncated ? slice(out, 0, limit) : out; + return r; +} + +// GL-parity trailing-hour view. Only range "hour" exists in v1; the argument +// is required so the contract can grow (day/week) without a breaking change. +function m_stats(req) { + let range = req.args?.range ?? "hour"; + if (range != "hour") + return ubus.STATUS_INVALID_ARGUMENT; + + let now = now_ms(); + let slot = int(now / SERIES_MS); + let r = envelope(now); + r.range = "hour"; + r.range_start = (slot - SERIES_SLOTS + 1) * SERIES_MS; + r.range_end = now; + + let t = ring_total(totals.buckets, slot, SERIES_SLOTS); + r.all = { download: t[0], upload: t[1], total: t[0] + t[1] }; + + let list = []; + for (let k in apps) { + let a = apps[k]; + let at = ring_total(a.buckets, slot, SERIES_SLOTS); + if ((at[0] + at[1]) == 0 && a.cur.flows == 0) + continue; + push(list, { + app: a.tag ?? a.key, + key: a.key, + label: a.name, + // An application's category is a property of its flows; the + // aggregate carries the most recent one seen. + category: a.cat ?? null, + download: at[0], + upload: at[1], + total: at[0] + at[1], + active_flows: a.cur.flows + }); + } + sort(list, (x, y) => y.total - x.total); + + // Only the leaders carry a time series -- twelve buckets per application + // would dominate the reply for no benefit. + let top = []; + for (let i = 0; i < length(list) && i < 10; i++) { + let e = list[i]; + push(top, { + app: e.app, key: e.key, label: e.label, category: e.category, + download: e.download, upload: e.upload, total: e.total, + active_flows: e.active_flows, + time_series: ring_series(apps[e.key].buckets, slot, SERIES_SLOTS, SERIES_MS) + }); + } + + r.applications = list; + r.top_apps = top; + return r; +} + +function m_status(req) { + let now = now_ms(); + let r = envelope(now); + r.version = VERSION; + r.uptime = int((mono_ms() - start_mono) / 1000); + r.started_at = start_ms; + r.socket = { path: cfg.socket_path, connected: connected, reconnects: stats.reconnects, + connect_failures: stats.connect_fail }; + r.config = { flow_max: cfg.flow_max, rate_window: cfg.rate_window, + top_n: cfg.top_n, idle_timeout: cfg.idle_timeout, + // 0/1 rather than a JSON boolean, so every value in this + // object is numeric and one reader parses all of them. + ai_breakout: cfg.ai_breakout ? 1 : 0 }; + r.flows = { tracked: nflows, seen: stats.flows_seen, late: stats.flows_late, + pruned: stats.flows_pruned, shed: stats.flows_shed, + dropped: stats.flows_dropped, + shadowed: stats.shadowed, purged: stats.purges, + no_total: stats.no_total, ai_labeled: stats.ai_labeled }; + // NAT twin resolution (3.2). `available` false means conntrack could not + // be read, in which case external captures are shadowed on the safe + // default and the router's own traffic is not attributed. A persistently + // high `unresolved` means entries are expiring before the flow is + // classified, which has the same effect for those flows. + r.conntrack = { available: conntrack.avail, entries: conntrack.entries, + twins: stats.ct_twins, router_local: stats.ct_local, + unresolved: stats.ct_unresolved }; + // Byte conservation (§2.5). `leaked` is what netifyd reported growing + // that neither reached an aggregate nor was deliberately shadowed as a NAT + // twin. It should sit at 0. A number that climbs is a byte-accounting + // defect, which is exactly the class that was previously invisible here. + r.bytes = { reported: stats.bytes_reported, + attributed: stats.bytes_attributed, + shadowed: stats.bytes_shadowed, + leaked: stats.bytes_reported - stats.bytes_attributed - stats.bytes_shadowed, + purge_only_rescued: stats.purge_only }; + r.aggregates = { apps: length(apps), devices: length(devs), + categories: length(cats), pruned: stats.aggs_pruned, + refused: stats.aggs_refused }; + r.events = { total: stats.events, frames: stats.frames, malformed: stats.malformed, + unknown: stats.unknown, unknown_types: stats.unknown_types, + by_type: stats.by_type, rx_bytes: stats.rx_bytes, resyncs: stats.resyncs, + last_event_age_ms: stats.last_event_ms ? (now - stats.last_event_ms) : -1 }; + r.agent = { version: agent.version, uptime: agent.uptime, + flow_count: agent.flow_count, maxrss_kb: agent.rss_kb ?? 0, + last_status_at: agent.ts }; + r.housekeeping = { sweeps: stats.sweeps, interval_ms: PRUNE_MS }; + r.memory = { rss_kb: rss_kb() }; + r.last_reset = stats.last_reset_ms; + return r; +} + +// "Clear" — zero every accumulator while keeping flow tracking intact, so the +// numbers restart from now without losing the live flow context. +function m_reset(req) { + let now = now_ms(); + agg_reset(totals, now); + for (let tbl in [ apps, devs, cats ]) + for (let k in tbl) { + agg_reset(tbl[k], now); + delete tbl[k].devs; + delete tbl[k].hosts; + } + // Zero each flow's attributed counters, but NOT flows[].total: that mirrors + // netifyd's cumulative lifetime counter and is the baseline the next + // flow_stats delta is measured against. Clearing it would make the next + // update compute a false multi-KB spike. flows[].lifetime_bytes therefore + // survives a reset by design. + for (let d in flows) { + flows[d].up = 0; + flows[d].down = 0; + } + stats.last_reset_ms = now; + logmsg("info", "statistics reset via ubus"); + let r = envelope(now); + r.reset = true; + return r; +} + +function ubus_publish(conn) { + // Every args policy accepts ubus_rpc_session: rpcd injects it into all + // LuCI-proxied calls, and an unknown typed field is rejected as EINVAL. + const SID = { ubus_rpc_session: "" }; + return conn.publish("appflow", { + summary: { call: m_summary, args: { limit: 0, ...SID } }, + /* `device` must be declared or ubus rejects the call outright, which + * presents as the method returning nothing at all. */ + apps: { call: m_apps, args: { sort: "", limit: 0, device: "", ...SID } }, + /* `apps` is an array of application keys. It MUST be declared here or + * ubus rejects the call outright rather than ignoring the argument, + * which presents as the method returning nothing at all. */ + devices: { call: m_devices, args: { sort: "", limit: 0, apps: [], ...SID } }, + app_detail: { call: m_app_detail, args: { app: "", ...SID } }, + flows: { call: m_flows, args: { limit: 0, app: "", device: "", ...SID } }, + stats: { call: m_stats, args: { range: "", ...SID } }, + status: { call: m_status, args: { ...SID } }, + reset: { call: m_reset, args: { ...SID } } + }); + // Reserved for §7.4: history { since, until } — deliberately not published + // in v1 so the method name stays free for the persistent-store version. +} + +/* ------------------------------------------------------------------ * + * main + * ------------------------------------------------------------------ */ + +function main() { + start_ms = now_ms(); + start_mono = mono_ms(); + + cfg_load(); + totals = agg_new("total", "total", "Total", start_ms); + load_categories(); + router_bootstrap(); + + uloop.init(); + + let conn; + try { + conn = ubus.connect(); + } catch (e) { + logmsg("error", "ubus connect failed: %s", e); + exit(1); + } + + try { + ubus_obj = ubus_publish(conn); + } catch (e) { + logmsg("error", "ubus publish failed: %s", e); + exit(1); + } + + for (let sig in [ "SIGTERM", "SIGINT" ]) + try { + uloop.signal(sig, function() { uloop.end(); }); + } catch (e) { + // signal handling is a nicety; procd will SIGKILL if we ignore it + } + + uloop.interval(PRUNE_MS, housekeeping); + + logmsg("info", "%s %s started (flow_max=%d rate_window=%ds)", + NAME, VERSION, cfg.flow_max, cfg.rate_window); + connect(); + + uloop.run(); + uloop.done(); + + if (sock) + try { sock.close(); } catch (e) {} + logmsg("info", "exiting"); +} + +main(); diff --git a/luci-app-appflow/root/usr/share/luci/menu.d/luci-app-appflow.json b/luci-app-appflow/root/usr/share/luci/menu.d/luci-app-appflow.json new file mode 100644 index 000000000..a5812e084 --- /dev/null +++ b/luci-app-appflow/root/usr/share/luci/menu.d/luci-app-appflow.json @@ -0,0 +1,32 @@ +{ + "admin/appflow": { + "title": "AppFlow", + "order": 15, + "action": { + "type": "firstchild", + "preferred": "overview", + "recurse": true + }, + "depends": { + "acl": [ "luci-app-appflow" ] + } + }, + + "admin/appflow/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "appflow/overview" + } + }, + + "admin/appflow/statistics": { + "title": "Statistics", + "order": 20, + "action": { + "type": "view", + "path": "appflow/statistics" + } + } +} diff --git a/luci-app-appflow/root/usr/share/rpcd/acl.d/luci-app-appflow.json b/luci-app-appflow/root/usr/share/rpcd/acl.d/luci-app-appflow.json new file mode 100644 index 000000000..b6c99fb67 --- /dev/null +++ b/luci-app-appflow/root/usr/share/rpcd/acl.d/luci-app-appflow.json @@ -0,0 +1,24 @@ +{ + "luci-app-appflow": { + "description": "Grant access to per-application traffic statistics. The 'apps' method is granted because the overview drills into a device to show what it is doing; it returns an aggregate per device and application from the live flow table. 'flows' remains ungranted: that is the per-connection table, with remote addresses and ports.", + "read": { + "ubus": { + "appflow": [ + "app_detail", + "apps", + "devices", + "stats", + "status", + "summary" + ] + } + }, + "write": { + "ubus": { + "appflow": [ + "reset" + ] + } + } + } +} diff --git a/luci-app-appflow/tests/frontend-suite.js b/luci-app-appflow/tests/frontend-suite.js new file mode 100755 index 000000000..987a66f05 --- /dev/null +++ b/luci-app-appflow/tests/frontend-suite.js @@ -0,0 +1,653 @@ +#!/usr/bin/env node +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 VolanticSystems +// +// appflow browser-half suite. +// +// WHY THIS FILE EXISTS. Every defect this package's frontend has had was found +// by a human or a review panel READING the code. Not one was found by a test, +// because until now no test could run any of it. The daemon has 54 checks +// against it and the JavaScript had none, which is backwards: the daemon +// mostly counts bytes, while the frontend is what decides whether an operator +// is told the collector is down. +// +// usage: node tests/frontend-suite.js +// +// Runs anywhere Node runs. No router, no browser, no network. The real +// common.js is loaded and executed by tests/luci-module.js; nothing in it is +// reimplemented here. +// +// EVERY CHECK IN THE FIRST THREE GROUPS IS A REGRESSION TEST FOR A DEFECT THAT +// WAS REAL, shipped, and fixed. They are not hypotheticals: each one names the +// date it was found and what it did to the screen. +// +// SABOTAGE COMMENTS: every check names the smallest edit to the PRODUCT that +// turns it red, written before the assertion. + +'use strict'; + +const path = require('path'); +const { load, potMsgids, textOf, classesOf } = require('./luci-module.js'); + +const ROOT = path.join(__dirname, '..'); +const COMMON = path.join(ROOT, 'htdocs/luci-static/resources/view/appflow/common.js'); +const POT = path.join(ROOT, 'po/templates/appflow.pot'); +const DAEMON = path.join(ROOT, 'root/usr/sbin/appflowd'); + +// The category column of the daemon's AI_HOSTS table, read out of the daemon +// source itself. This is the only cross-file contract the browser half has: if +// the daemon emits a category the frontend has no key for, the label is +// untranslatable and nothing else in either suite notices. +// +// Deliberately a text scan rather than a fixture. A fixture would record what +// the daemon USED to send, which is exactly the failure being guarded against. +function daemonAiCategories() { + const src = require('fs').readFileSync(DAEMON, 'utf8'); + const out = new Set(); + const re = /\[\s*"[^"]*"\s*,\s*"(ai-[a-z-]+|AI [a-z]+)"\s*,/g; + let m; + while ((m = re.exec(src)) !== null) out.add(m[1]); + return [...out].sort(); +} + +let PASS = 0, FAIL = 0; + +function ok(m) { PASS++; console.log(' PASS ' + m); } +function bad(m) { FAIL++; console.log(' FAIL ' + m); } +function chk(desc, expected, actual) { + const e = JSON.stringify(expected), a = JSON.stringify(actual); + if (e === a) ok(desc); else bad(`${desc} (expected ${e}, got ${a})`); +} +function head(m) { console.log('\n=== ' + m + ' ==='); } + +// rpc and request throw if touched. Nothing under test calls them, and a stub +// that answered plausibly would let a test pass against a product that had +// stopped asking. +const c = load(COMMON, { + rpc: { declare: (o) => () => { throw new Error('rpc called: ' + (o && o.method)); } }, + request: { get: () => { throw new Error('request called'); } } +}); + +// --------------------------------------------------- the status strip + +head('STATUS STRIP: absent evidence is not positive evidence'); + +// THE DEFECT, found 2026-08-25. The test was +// connected = (s.agent_connected !== false) && (sock.connected !== false) +// and `undefined !== false` is true, so a payload carrying neither field +// rendered a single green "Netify agent connected" pill. Overview passes +// `status || summary`, so this fired exactly when the status call FAILED: the +// one indicator whose job is to say the collector is down failed GREEN, on the +// most common reason for the page being empty. +// +// SABOTAGE for this whole group: in statusStrip(), drop the `known &&` from +// the `connected` expression, or replace `known` with `true`. The three +// unknown rows go red and the two positive rows stay green, which is the +// asymmetry that matters. +const STATES = [ + ['no status at all', undefined, 'af-unknown'], + ['an empty object', {}, 'af-unknown'], + // The payload that actually triggered it: a summary, passed in because the + // status call failed. It has neither field. + ['a summary payload (the real case)', { bytes_total: 12345, top_apps: [] }, 'af-unknown'], + ['agent_connected true', { agent_connected: true }, 'af-ok'], + ['agent_connected false', { agent_connected: false }, 'af-warn'], + // socket.connected alone IS positive evidence: it says the daemon has the + // export socket open. `known` requires only that ONE of the two fields be + // present, which is deliberate. + ['socket.connected true only', { socket: { connected: true } }, 'af-ok'], + ['socket.connected false only', { socket: { connected: false } }, 'af-warn'] +]; + +for (const [label, status, wantClass] of STATES) { + const cls = classesOf(c.statusStrip(status)); + if (cls.has(wantClass)) ok(`${label} -> ${wantClass}`); + else bad(`${label} -> expected ${wantClass}, got [${[...cls].join(' ')}]`); +} + +// And the text, because a neutral class with reassuring words is still a lie. +// +// SABOTAGE: change the unknown-state string back to "Netify agent connected". +// The class check above stays green and only this goes red. +const unknownText = textOf(c.statusStrip({ bytes_total: 1 })); +if (/unknown/i.test(unknownText)) ok('and the unknown state SAYS unknown'); +else bad(`the unknown state reads "${unknownText}"`); + +// -------------------------------------------- prototype-chain lookups + +head('HOSTILE KEYS: a name from the wire must not reach Object.prototype'); + +// THE DEFECT, found 2026-08-25. CATEGORY_SLOT is an object literal, so it +// inherits Object.prototype, and a bare `CATEGORY_SLOT[s] != null` test passed +// for 'constructor', '__proto__', 'toString' and friends, indexing PALETTE +// with a function and yielding undefined. A device named `constructor` +// rendered style="background:undefined", which the browser drops: white text +// on a transparent tile, and in the doughnut the slice silently did not draw +// while its legend still claimed a percentage. +// +// `s` is attacker-influenced. It comes from a category tag, an application +// label, or a DHCP hostname, so a LAN device can choose it. +// +// SABOTAGE: in color(), replace +// Object.prototype.hasOwnProperty.call(CATEGORY_SLOT, s) +// with a bare `CATEGORY_SLOT[s] != null`. Every row below goes red. +const HOSTILE = ['constructor', '__proto__', 'toString', 'hasOwnProperty', + 'valueOf', 'isPrototypeOf']; + +for (const key of HOSTILE) { + const col = c.color(key); + if (typeof col === 'string' && /^hsl\(/.test(col)) + ok(`color(${JSON.stringify(key)}) is a real colour`); + else + bad(`color(${JSON.stringify(key)}) returned ${JSON.stringify(col)}`); +} + +// The mirror: a function that returned a fixed colour for everything would +// pass all six rows above. A known category must still get its ASSIGNED slot, +// not the hash fallback. +// +// SABOTAGE: delete the hasOwnProperty branch entirely so everything falls +// through to the hash. This goes red while the six rows above stay green. +const known = c.color('games'), other = c.color('games'); +chk('a known category is stable across calls', known, other); +if (c.color('games') !== c.color('shopping')) + ok('and different categories get different colours'); +else + bad('two different categories returned the same colour'); + +// -------------------------------------------------------- normalisers + +head('LABELS: netifyd slugs are cased, deliberate casing is left alone'); + +// catLabel() used to lowercase its input before title-casing it. That is +// harmless for netifyd's own categories, which are lowercase slugs, and it +// destroyed appflow's AI categories: "AI assistants" came back as +// "Ai assistants", because the title-case split is on [-_] and a space is +// neither. It rendered that way on a live router before anyone noticed. +// +// Since 1.1.1 the daemon sends those four as slugs, so no category the daemon +// actually emits reaches the pass-through any more. It is kept because it is +// the guard for anything NOT in the table, and these two inputs exercise it +// directly. +// +// SABOTAGE: remove the /[A-Z]/ pass-through in catLabel(). The first two go +// red and the netifyd cases stay green, which is the distinction that matters. +chk('a category with deliberate capitals is left alone', + 'AI assistants', c.catLabel('AI assistants')); +chk('and so is a second one', 'AI infrastructure', c.catLabel('AI infrastructure')); + +// The netifyd side must be unchanged by that pass-through. +chk('a lowercase netifyd slug is still title-cased', 'Networking', c.catLabel('networking')); +chk('and a hyphenated one still splits', 'Social Network', c.catLabel('social-network')); +chk('an empty category is still Unclassified', 'Unclassified', c.catLabel('')); + +head('TILE LETTERS: a name in any script must produce a letter from that script'); + +// THE DEFECT, found 2026-08-31 by rendering the real component and looking at +// it. tile() stripped leading characters with /^[^0-9A-Za-z]+/, an ASCII class, +// so a name in any non-Latin script was consumed entirely and fell through to +// the '?' fallback. Every Chinese, Japanese, Korean, Russian, Greek, Arabic and +// Hebrew name rendered an identical question-mark tile. +// +// It was never a translation bug. Any LAN client with a non-Latin DHCP hostname +// hit it on every release shipped so far; accepting a zh_Hans catalogue would +// have turned an occasional case into a universal one. +// +// SABOTAGE: put the ASCII class back. The three non-Latin rows go red and every +// Latin row stays green, which is the asymmetry that matters. +chk('a Latin name is unchanged', 'R', textOf(c.tile('Router', 'Router'))); +chk('and a hyphenated one', 'K', textOf(c.tile('kitchen-pi', 'kitchen-pi'))); +chk('Chinese yields a Chinese character', '路', + textOf(c.tile('路由器', '路由器'))); +chk('Japanese yields a Japanese character', '日', + textOf(c.tile('日本語', '日本語'))); +chk('Cyrillic yields a Cyrillic character, upper-cased', 'Р', + textOf(c.tile('Русский', + 'Русский'))); + +// The strip must still do its job, and the fallback must still exist. A fix +// that simply removed the strip would pass every row above. +// +// SABOTAGE: delete the .replace() entirely. This row goes red. +chk('leading punctuation is still stripped', 'W', textOf(c.tile('...weird', 'x'))); +// SABOTAGE: replace the '?' fallback with ''. This row goes red. +chk('a nameless tile still falls back', '?', textOf(c.tile('', 'x'))); + +head('LABELS ARE TRANSLATABLE, WHICH IS NOT WHAT THE CHECKS ABOVE PROVE'); + +// THE DEFECT, found 2026-08-30, reported by @ntbowen against 1.1.0. +// +// READ THIS BEFORE ADDING A CHECK ABOVE INSTEAD OF HERE. Every assertion in +// the group above is satisfied identically by two implementations, one of +// which is broken: +// +// catLabel('networking') -> _('Networking') translatable +// catLabel('networking') -> 'networking'.replace(…) English for ever +// +// Both return the string 'Networking', so `chk(…, 'Networking', …)` passes on +// either. The difference IS the feature, and an equality assertion cannot see +// it. Three separate label paths shipped broken underneath a green run of this +// file, including its own catalogue check, which asserts that every string +// reaching _() is in the .pot and is therefore blind by construction to a +// string that never reaches _() at all. +// +// The observable that distinguishes them is not the output, it is WHETHER _() +// WAS CALLED. tests/luci-module.js records every string the module passes to +// _() as it loads, so the assertion is membership in that set. +// +// NETIFYD_TAGS is a deliberate second copy, read off the bench from +// /etc/netify.d/netify-categories.json on netifyd 4.4.7 (32 application tags +// + 18 protocol tags, 43 unique). It must NOT be imported from the product, +// or this check becomes a tautology that compares the table with itself. +const NETIFYD_TAGS = [ + 'adult', 'advertiser', 'authentication', 'business', 'cdn', 'cybersecurity', + 'database', 'device-iot', 'education', 'entertainment', 'file-server', + 'file-sharing', 'financial', 'gambling', 'games', 'government', 'hosting', + 'infrastructure', 'mail', 'malware', 'media', 'media-provider', 'messaging', + 'networking', 'news', 'os-software-updates', 'portal', 'printing', 'proxy', + 'recreation', 'reference', 'remote-desktop', 'shopping', 'social-media', + 'sports', 'streaming-media', 'technology', 'telco', 'unclassified', 'voip', + 'vpn', 'vpn-and-proxy', 'web' +]; + +// THE MODULE IS LOADED A SECOND TIME WITH A MARKING TRANSLATOR, and every +// check below runs against that copy rather than against `c`. +// +// The first version of this group asked "is the returned string in the set of +// strings passed to _()". That is weaker than it looks, and a panel named the +// general shape after an executed sabotage had already found one instance: +// with the default identity translator, `_('Web')` and a runtime title-caser +// both produce exactly `'Web'`, so a membership test can be satisfied by a +// COINCIDENCE between an untranslated label and some other string that did go +// through _(). It happened for real with the AI categories, where the table +// entry _('AI media') put that exact string in the set, so a daemon that had +// reverted to sending 'AI media' still passed. +// +// With `_()` returning `[[msgid]]`, a translated label and a generated one are +// different strings, and plain equality becomes a sound test again. `c` keeps +// the identity translator because every other check in this file asserts on +// real English output. +const T = (s) => `[[${s}]]`; +const t9n = load(COMMON, { + rpc: { declare: (o) => () => { throw new Error('rpc called: ' + (o && o.method)); } }, + request: { get: () => { throw new Error('request called'); } } +}, { translate: T }); + +// __translated still records the ARGUMENT to _(), not its return, so the +// catalogue check further down is unaffected by the marking translator. +const translated = new Set(c.__translated.filter(Boolean)); + +// A check over an empty set passes vacuously. Refuse to report on one, the +// same way the catalogue check below does. +if (translated.size === 0) { + bad('the run reached NO _() strings, so nothing below proves anything'); +} else if (t9n.normDevice({}).name !== T('Unknown device')) { + // If the marking translator is not actually reaching the product, every + // equality check below would compare English with English and pass while + // proving nothing. Prove the instrument works before trusting it. + // + // ANCHOR THIS ON A STRING THAT IS NOT IN EITHER TABLE UNDER TEST. The + // first version probed catLabel('web'), which is one of the very entries + // these checks exist to police: unwrapping that entry to `'web': 'Web'` + // made the probe fail, so a real product defect reported itself as a + // broken instrument and skipped the nine checks that would have named it. + // normDevice's fallback is translated on a path no check below asserts on. + bad('the marking translator had no effect, so nothing below proves anything'); +} else { + // SABOTAGE: delete any one entry from CATEGORY_LABELS in common.js, or + // write it unwrapped as `'web': 'Web'`. The title-caser still returns the + // same English text and every equality check above stays green; this goes + // red and names the tag. + const untranslatable = NETIFYD_TAGS.filter((t) => t9n.catLabel(t) !== T(c.catLabel(t))); + if (untranslatable.length === 0) + ok(`all ${NETIFYD_TAGS.length} netifyd category tags return the translator's output`); + else + bad(`${untranslatable.length} category tag(s) produce a label _() never saw, ` + + `so they are English in every language: ${JSON.stringify(untranslatable)}`); + + // Every one of those labels must also be in the shipped catalogue. The + // check above proves _() was called; this proves the extractor saw it. + // + // SABOTAGE: add a category entry to common.js without regenerating the + // .pot. This goes red; the check above stays green. + const catalogue = potMsgids(POT); + const uncatalogued = NETIFYD_TAGS.map((t) => c.catLabel(t)) + .filter((s) => !catalogue.has(s)); + if (uncatalogued.length === 0) + ok('and every one of those labels is in appflow.pot'); + else + bad(`${uncatalogued.length} label(s) reach _() but are not in the catalogue: ` + + JSON.stringify(uncatalogued.slice(0, 5))); + + // The daemon's own AI categories, which is where WE introduced this bug + // rather than inheriting it. Until 1.1.1 appflowd sent 'AI assistants' and + // three siblings as rendered English, and catLabel returned them untouched. + // + // TWO ASSERTIONS, AND THE FIRST ONE IS NOT REDUNDANT. The membership test + // alone cannot catch this: catLabel('AI media') falls through the + // pass-through and returns 'AI media' unchanged, and that exact string IS + // in the translated set, because the table entry 'ai-media': _('AI media') + // put it there. So a daemon that reverted to display strings would satisfy + // a membership check while being exactly as broken as before. That was the + // first version of this check and an executed sabotage caught it, which is + // the only reason it is written this way. + // + // SABOTAGE A: change one AI_HOSTS category in appflowd back to a display + // string, e.g. 'ai-media' -> 'AI media'. The shape check goes red. + // SABOTAGE B: change one to a slug with no table entry, e.g. 'ai-video'. + // The membership check goes red. + const SLUG = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/; + const daemonCats = daemonAiCategories(); + if (daemonCats.length < 4) { + bad(`read only ${daemonCats.length} AI categories out of appflowd; expected 4`); + } else { + const shaped = daemonCats.filter((t) => !SLUG.test(t)); + if (shaped.length === 0) + ok(`all ${daemonCats.length} AI categories the daemon emits are lowercase slugs`); + else + bad(`the daemon emits pre-rendered display string(s) ${JSON.stringify(shaped)}; ` + + 'a category that arrives already in English has no key to look up'); + + const unknown = daemonCats.filter((t) => t9n.catLabel(t) !== T(c.catLabel(t))); + if (unknown.length === 0) + ok(`and the frontend has a _() label for all ${daemonCats.length} of them`); + else + bad(`the daemon emits ${JSON.stringify(unknown)}, which the frontend ` + + 'has no CATEGORY_LABELS entry for'); + } + + // The three synthetic pseudo-devices. The daemon sends their display name + // already rendered in English because it has no idea what language the + // browser wants, so the frontend must name them from the stable key. + // + // The fixture's `name` is deliberately the real English the daemon sends, + // not a marker string: a lookup keyed on the name rather than the key is a + // plausible implementation and would pass against a fixture that could not + // satisfy it. + // + // SABOTAGE: delete an entry from DEVICE_LABELS, key the lookup on the + // daemon's name instead of t.key, or misspell a key. This goes red. + [ [ 'router', 'Router' ], + [ 'unknown', 'Unknown' ], + [ 'multicast', 'Multicast / Broadcast' ] ].forEach(function(pair) { + const key = pair[0], english = pair[1]; + const got = t9n.normDevice({ key: key, name: english }).name; + if (got === T(english)) + ok(`pseudo-device '${key}' is named through _(), not from the daemon's English`); + else + bad(`pseudo-device '${key}' rendered ${JSON.stringify(got)}, ` + + `expected the translated ${JSON.stringify(T(english))}`); + }); + + // A real device must NOT be captured by that table, even when its DHCP + // hostname is exactly the English of a pseudo-device. This is the check + // that a name-keyed implementation fails. + // + // SABOTAGE: index DEVICE_LABELS by the daemon's name rather than t.key. + [ 'kitchen-pi', 'Router', 'Multicast / Broadcast' ].forEach(function(name) { + const got = t9n.normDevice({ key: 'b4:2e:99:3a:d6:df', + mac: 'b4:2e:99:3a:d6:df', name: name }).name; + if (got === name) + ok(`a real device named ${JSON.stringify(name)} keeps its own name`); + else + bad(`a real device named ${JSON.stringify(name)} was renamed to ` + + `${JSON.stringify(got)} by the pseudo-device table`); + }); +} + +// Both label tables are plain object literals, so they inherit +// Object.prototype and a bare lookup on an inherited key returns a truthy +// non-string. t.name is assumed to be a string by the .toUpperCase() two lines +// later in normDevice, and a label is assumed to be a string by every caller. +// +// Raised independently by two reviewers against the contributor's patch, which +// used a bare `DEVICE_LABELS[key] ||` lookup. color() in the same file already +// guarded its own table this way; catLabel() did not. +// +// THESE PROBES FEED HOSTILE INPUT, SO THEY MUST NOT BE ALLOWED TO THROW +// UNCAUGHT. Removing the guard makes normDevice raise a TypeError rather than +// return a bad value, and an uncaught throw here would abort the whole file: +// node prints a stack trace, every later check never runs, and the summary +// line that says how many passed is never printed at all. The run is still red +// by exit code, but "red" and "red naming the defect, with the other 60 checks +// still reported" are different things. attempt() turns a throw into a named +// failure and lets the suite finish. +// +// SABOTAGE: replace either hasOwnProperty guard with a bare truthiness test. +// Every probe below goes red, by value for catLabel and by throw for +// normDevice, and the checks after this block still report. +function attempt(desc, fn) { + let v; + try { + v = fn(); + } catch (e) { + bad(`${desc} threw ${e && e.name}: ${e && e.message}`); + return; + } + if (typeof v === 'string') ok(desc); + else bad(`${desc} produced a ${typeof v}, not a string`); +} + +[ '__proto__', 'constructor', 'toString', 'hasOwnProperty' ].forEach(function(k) { + attempt(`catLabel(${JSON.stringify(k)}) returns a string`, + () => c.catLabel(k)); + attempt(`normDevice key ${JSON.stringify(k)} names a string`, + () => c.normDevice({ key: k, mac: 'aa:bb:cc:dd:ee:ff' }).name); +}); + +head('NORMALISERS: idempotent, and never inventing a total'); + +// THE DEFECT, found 2026-08-25. normSeries() was not idempotent and one caller +// normalised twice. The second pass read the already-renamed fields, found +// nothing, and produced {dl:0, ul:0, total:}: a total contradicting its +// own parts. The chart drew empty while the "no data" message stayed +// suppressed, because `total` was non-zero. +// +// SABOTAGE: remove 'dl' and 'ul' from the alias lists the normaliser accepts. +// The second pass zeroes them and this goes red. +const rawSeries = [{ time: 1, dl: 10, ul: 5 }, { time: 2, dl: 20, ul: 10 }]; +const once = c.normSeries(rawSeries); +const twice = c.normSeries(once); +chk('normSeries is idempotent', once, twice); + +// SABOTAGE: change the `(tot >= 0) ? tot : dl + ul` fallback to a constant. +chk('a point with no explicit total gets dl+ul', 15, once[0].total); +chk('and the parts survive', [10, 5], [once[0].dl, once[0].ul]); + +// The invariant that would have caught the original defect on its own, stated +// as an invariant rather than as a value: parts and total must not contradict. +const contradictions = twice.filter((p) => p.dl === 0 && p.ul === 0 && p.total > 0); +chk('no point claims a total while both parts are zero', [], contradictions); + +// SABOTAGE: make normSeries() return its input unchanged. The empty and +// absent cases stop being normalised and this goes red. +chk('normSeries(null) is an empty list', [], c.normSeries(null)); +chk('normSeries(undefined) is an empty list', [], c.normSeries(undefined)); + +head('HELPERS: the accessors every view depends on'); + +// SABOTAGE for each: change the default-value branch of num()/str()/toList() +// so a miss returns something other than the supplied default. +chk('num() finds a value by alias', 5, c.num({ a: 5 }, ['x', 'a'], 0)); +chk('num() returns the default on a miss', 42, c.num({}, ['nope'], 42)); +chk('num() returns the default on a non-number', 7, c.num({ a: 'xyz' }, ['a'], 7)); +chk('str() returns the default on a miss', 'dflt', c.str({}, ['nope'], 'dflt')); +chk('toList(null) is empty', [], c.toList(null)); +chk('toList of an array is itself', [1, 2], c.toList([1, 2])); + +// toList over an object keyed by name must carry the key through, or every +// per-device row loses its identity. +// +// SABOTAGE: drop the keyField assignment in toList(). This goes red. +const keyed = c.toList({ 'aa:bb': { bytes: 1 } }, 'mac'); +chk('toList carries the object key into the named field', 'aa:bb', keyed[0] && keyed[0].mac); + +// ---------------------------------------------------------- catalogue + +head('FILTER: include, exclude, and the shapes a half-typed term takes'); + +// WHY THIS GROUP EXISTS. The filter is the only way to make a busy dashboard +// legible, and the question that produced it was concrete: one streaming +// session buried everything else and there was no way to look past it. +// +// Both views share this logic precisely so they cannot drift, and it lives in +// common.js rather than inside a render function so a test can reach it at all. +// The classifier lesson from the sibling package: logic that decides something +// belongs where Node can load it. + +// A term matches the application label, its key, or its category label. Those +// are the three strings a person could plausibly be typing at. +const FIELDS = { + claude: [ 'Anthropic (Claude)', 'ai:anthropic-claude', 'AI assistants' ], + openai: [ 'OpenAI', 'ai:openai', 'AI assistants' ], + netflix: [ 'Netflix', 'a145', 'Streaming Media' ], + http: [ 'HTTP/S', 'p196', 'Web' ] +}; + +const pass = (text, row) => c.matchFilter(c.parseFilter(text), FIELDS[row]); + +// An empty filter passes everything. This is the unfiltered case and must not +// be special-cased by any caller. +// +// SABOTAGE: make matchFilter return false for an empty term list. Every view +// renders an empty table on load, which is as broken as it sounds. +chk('an empty filter passes everything', [ true, true, true, true ], + [ 'claude', 'openai', 'netflix', 'http' ].map((r) => pass('', r))); + +// Matching is case-insensitive and matches any of the three fields. +chk('a term matches the application label', true, pass('netflix', 'netflix')); +chk('and is case-insensitive', true, pass('NeTfLiX', 'netflix')); +chk('a term matches the CATEGORY label', true, pass('ai assistants', 'claude')); +chk('a partial category matches', true, pass('ai', 'claude')); +chk('a term matches the internal key', true, pass('p196', 'http')); +chk('a term that matches nothing excludes', false, pass('netflix', 'claude')); + +// EXCLUSION. This is the case the whole feature exists for: "show me +// everything except the thing that is burying the view". An include-only +// filter cannot express it, because you would have to already know what you +// wanted to keep. +// +// SABOTAGE: drop the leading-minus branch in parseFilter so '-netflix' is +// treated as an ordinary term. The two checks below invert. +chk('a minus term removes the row it matches', false, pass('-netflix', 'netflix')); +chk('and leaves every other row alone', true, pass('-netflix', 'claude')); + +// Terms combine with AND, so each one narrows. Mixing include and exclude is +// the useful case: this category, except that member of it. +// +// SABOTAGE: change every() to some() in matchFilter. The first goes true. +chk('include and exclude combine', false, pass('ai -claude', 'claude')); +chk('and the combination still includes', true, pass('ai -claude', 'openai')); +chk('two includes both have to match', false, pass('ai netflix', 'claude')); + +// A bare '-' is what a half-typed exclusion looks like. Treating it as +// "exclude everything" would blank the table between two keystrokes. +// +// SABOTAGE: remove the final filter() in parseFilter that drops empty terms. +chk('a bare minus is ignored, not treated as exclude-all', + [ true, true ], [ pass('-', 'claude'), pass('-', 'netflix') ]); +chk('and so is stray whitespace', true, pass(' ', 'claude')); + +// Nulls and absent fields must not throw: a row can legitimately have no +// category, and normApp fills what it can. +// +// SABOTAGE: drop the null guard in matchFilter's field join. +chk('a row with missing fields does not throw', false, + c.matchFilter(c.parseFilter('claude'), [ null, undefined, '' ])); +chk('and still matches on the field it does have', true, + c.matchFilter(c.parseFilter('claude'), [ null, 'Anthropic (Claude)', undefined ])); + +head('DEVICE IDENTITY: the key the drill-down is addressed by'); + +// normDevice DROPPED the daemon's device key. Every call site that needed it +// got undefined, so clicking a device cleared the selection instead of setting +// it: no error, no effect, a feature that looked wired and was not. Three bugs +// in one evening had that exact shape. +// +// SABOTAGE: remove `t.key = ...` from normDevice. This goes red and nothing +// else does, which is precisely why it had to be added. +{ + const d = c.normDevice({ key: 'b4:2e:99:3a:d6:df', mac: 'b4:2e:99:3a:d6:df', + ip: '192.168.72.20', name: 'Stang', bytes_total: 10 }); + + chk('normDevice carries the daemon key through', 'b4:2e:99:3a:d6:df', d.key); + chk('and still carries name, mac and ip', [ 'Stang', 'B4:2E:99:3A:D6:DF', '192.168.72.20' ], + [ d.name, d.mac, d.ip ]); +} + +// The pseudo-devices have no MAC at all, so a fallback to mac/ip would leave +// them unaddressable. The daemon's key is what they are called. +// +// SABOTAGE: change the key preference order so mac wins over key. +{ + const d = c.normDevice({ key: 'router', name: 'Router', bytes_total: 5 }); + + chk('a pseudo-device is addressed by its key', 'router', d.key); +} + +head('CLICKABLE CELLS: a label that names a thing is a way to see it'); + +// appCell and deviceCell take an onPick callback. Without one they render +// exactly as before, so nothing that does not want the behaviour gets it. +// +// SABOTAGE: make appCell add af-pick unconditionally. The first check goes red, +// and every table that never wanted click targets grows them. +{ + const plain = c.appCell({ label: 'Netflix', key: 'a145', cat: 'streaming-media' }); + chk('without a callback an app cell is not clickable', + false, /af-pick/.test(JSON.stringify(plain))); + + const picked = []; + const live = c.appCell({ label: 'Netflix', key: 'a145', cat: 'streaming-media' }, + null, (v) => picked.push(v)); + chk('with a callback it is', true, /af-pick/.test(JSON.stringify(live))); +} + +// A device cell with a callback is clickable and passes the whole device +// object, because the caller needs both the key (to query) and the name (to +// show in the chip). +// +// SABOTAGE: have deviceCell pass dev.name instead of dev. The drill-down then +// queries by name, which is not what the daemon keys on. +{ + const plain = c.deviceCell({ name: 'Stang', mac: 'B4:2E:99:3A:D6:DF', key: 'b4:2e' }); + chk('without a callback a device cell is not clickable', + false, /af-pick/.test(JSON.stringify(plain))); + + const live = c.deviceCell({ name: 'Stang', mac: 'B4:2E:99:3A:D6:DF', key: 'b4:2e' }, + () => {}); + chk('with a callback it is', true, /af-pick/.test(JSON.stringify(live))); +} + +head('THE STRING CATALOGUE COVERS WHAT THE CODE ASKS FOR'); + +// SABOTAGE: add a `_('some new string')` anywhere in common.js without +// regenerating po/templates/appflow.pot. This goes red. Without it the +// catalogue rots silently the moment anyone adds a message, and nobody finds +// out until a translator does. +const pot = potMsgids(POT); +const seen = new Set(c.__translated.filter(Boolean)); + +// A catalogue check over an empty set proves nothing, so refuse to report a +// pass on one. +if (seen.size === 0) { + bad('the run reached NO _() strings, so this check proved nothing'); +} else { + const missing = [...seen].filter((x) => !pot.has(x)); + if (missing.length === 0) + ok(`all ${seen.size} strings reached by this run are in the catalogue`); + else + bad(`${missing.length} of ${seen.size} not in appflow.pot: ` + + JSON.stringify(missing.slice(0, 5))); +} + +// The catalogue must also not be EMPTY or trivially small, which would make +// the check above pass while covering nothing. +// +// SABOTAGE: truncate appflow.pot. This goes red. +if (pot.size >= 100) ok(`the catalogue holds ${pot.size} msgids`); +else bad(`the catalogue holds only ${pot.size} msgids; expected the full extraction`); + +console.log('\n----------------------------------------'); +console.log(`passed ${PASS}, failed ${FAIL}`); +process.exit(FAIL); diff --git a/luci-app-appflow/tests/hardware-suite.sh b/luci-app-appflow/tests/hardware-suite.sh new file mode 100755 index 000000000..759078c69 --- /dev/null +++ b/luci-app-appflow/tests/hardware-suite.sh @@ -0,0 +1,422 @@ +#!/bin/sh +# appflow hardware test suite. +# +# The behavioural claims this package makes that can be checked without a +# browser, as one runnable script. It exists because all of them had previously +# been verified only by ad-hoc commands typed once and thrown away, and because +# two of this daemon's defects were silent: they produced wrong numbers while +# every status field said things were fine. +# +# RUN IT ON A SANDBOX ROUTER. It restarts appflowd, resets its counters, and +# generates traffic. It does not modify configuration. +# +# usage: sh hardware-suite.sh run everything +# sh hardware-suite.sh conservation run one group +# (health|conservation|conntrack|restart|acl) +# +# Exit status is the number of failed checks. +# +# WHAT THIS SUITE CAN AND CANNOT CATCH, established by running it against the +# pre-fix v1.0.1 daemon on 2026-08-25, which is the only test of a test worth +# anything: +# +# caught: an old daemon with no conservation counters at all +# (status.bytes missing), and any leak once they exist +# NOT caught: the reshadow double-count itself. The restart ratio came back +# 1.012 on the known-broken daemon, having been 0.70 and 0.79 on +# two runs the day before. See the note in test_restart. +# +# Unlike the wansentry suite, nothing here reimplements the code under test: +# every check queries the running daemon over ubus. The weakness is coverage, +# not construction. +# +# WIRE TRUTH. Where a test needs to know how many bytes really moved, it reads +# the client veth's own counter in /sys/class/net. Nothing in appflowd can +# influence that number, which is the entire point: comparing appflow against +# appflow proves nothing. +# +# Requires: appflowd and netifyd running, root, and a network namespace client +# (see NETNS below) for the NAT tests. Groups that need the netns skip +# themselves with a clear message when it is absent rather than failing. + +set -u + +GROUP="${1:-all}" +NETNS="${NETNS:-cli}" +VETH="${VETH:-veth-host}" +PASS=0; FAIL=0; SKIP=0 + +head2() { printf '\n=== %s ===\n' "$*"; } +ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$*"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$*"; } +skip() { SKIP=$((SKIP+1)); printf ' SKIP %s\n' "$*"; } + +st() { ubus call appflow status 2>/dev/null; } +# field e.g. field bytes leaked +field() { + st | sed -n "/\"$1\"/,/}/p" | sed -n "s/.*\"$2\": *\([0-9-]*\).*/\1/p" | head -1 +} +wire() { cat "/sys/class/net/$VETH/statistics/tx_bytes" 2>/dev/null || echo 0; } + +have_netns() { + ip netns list 2>/dev/null | grep -q "^$NETNS" && [ -r "/sys/class/net/$VETH/statistics/tx_bytes" ] +} + +# A sustained NAT-ed transfer. The WAN here moves 50 MB in about a second, so an +# ordinary download finishes long before anything interesting happens; the +# throttle is TCP backpressure from a consumer that reads a fixed amount per +# second. head -c reads an exact count, dd on a pipe does short reads and +# throttles to a trickle instead. +slow_pull() { # slow_pull + local secs="$1" bps="$2" + ip netns exec "$NETNS" sh -c " + { printf 'GET /__down?bytes=40000000 HTTP/1.0\r\nHost: speed.cloudflare.com\r\nConnection: close\r\n\r\n' + sleep $secs + } | socat - TCP:speed.cloudflare.com:80 2>/dev/null | { + n=0 + while [ \"\$n\" -lt $secs ]; do + head -c \$(( $bps * 65536 )) > /dev/null 2>&1 || break + n=\$(( n + 1 )); sleep 1 + done + }" >/dev/null 2>&1 +} + +ratio() { awk -v a="$1" -v w="$2" 'BEGIN{ if (w<=0) print "0"; else printf "%.3f", a/w }'; } +within() { # within + awk -v v="$1" -v lo="$2" -v hi="$3" 'BEGIN{ exit !(v>=lo && v<=hi) }' +} + +# ---------------------------------------------------------------- health + +test_health() { + head2 "DAEMON HEALTH" + + # busybox pgrep has no -c, so count the lines rather than asking it to. + if pgrep -f '[a]ppflowd' >/dev/null 2>&1; then + ok "appflowd is running" + else + bad "appflowd is not running"; return + fi + + [ -n "$(st)" ] && ok "ubus object 'appflow' answers" \ + || bad "ubus call appflow status returned nothing" + + case "$(st | sed -n 's/.*"connected": *\(true\|false\).*/\1/p' | head -1)" in + true) ok "connected to netifyd's export socket" ;; + *) bad "not connected to netifyd" ;; + esac + + # A daemon that has never seen an event is not a healthy daemon, it is a + # daemon whose socket happens to be open. + # Default the value: an empty field makes [ -gt ] abort the whole script with + # "out of range" instead of simply failing this one check. + local seen; seen=$(field flows seen); seen=${seen:-0} + if [ "$seen" -gt 0 ] 2>/dev/null; then + ok "flows have been observed (seen=$seen)" + else + bad "no flows observed at all" + fi +} + +# ---------------------------------------------------------- conservation + +test_conservation() { + head2 "BYTE CONSERVATION (reported == attributed + shadowed)" + # Both silent byte-loss defects this daemon has had would have shown here in + # the first minute. This is the check that makes them loud. + + have_netns || { skip "needs netns '$NETNS' (conservation)"; return; } + + ubus call appflow reset >/dev/null 2>&1; sleep 2 + slow_pull 20 6 + sleep 25 + + local rep att sh lk + rep=$(field bytes reported); att=$(field bytes attributed) + sh=$(field bytes shadowed); lk=$(field bytes leaked) + + if [ -z "$rep" ] || [ -z "$lk" ]; then + bad "status.bytes is missing (is this appflow >= 1.0.2?)"; return + fi + printf ' reported=%s attributed=%s shadowed=%s leaked=%s\n' "$rep" "$att" "$sh" "$lk" + + [ "$lk" = "0" ] && ok "leaked == 0 (nothing reported went unaccounted)" \ + || bad "leaked == $lk, so bytes were reported and never attributed" + + [ "${rep:-0}" -gt 0 ] 2>/dev/null && ok "traffic was actually observed" \ + || bad "no bytes reported; the transfer did not happen" + + # purge_only_rescued counts flows that were purged having never received a + # flow_stats. Those carry their whole byte count on the purge event, and + # before this path existed every one of them was silently discarded. + # + # It is NOT expected to be zero, and an earlier version of this suite + # asserted that it was. Measured on this hardware: 13 of 1341 flows, 0.97%. + # A 38-flow event-socket capture had seen none, which at that rate has an + # expected count of 0.4 -- so the sample was far too small to support the + # "netifyd never does this" conclusion that was briefly drawn from it, and + # the review panel that reported the byte loss was right. + # + # What is worth watching is the RATE. A sudden jump means netifyd's event + # contract has shifted; zero across a busy run means this path stopped + # firing and the rescue may have regressed. + local po seen; po=$(field bytes purge_only_rescued); po=${po:-0} + seen=$(field flows seen); seen=${seen:-0} + if [ "$seen" -gt 200 ] 2>/dev/null; then + printf ' purge_only_rescued=%s of %s flows (%s%%)\n' "$po" "$seen" \ + "$(awk -v p="$po" -v s="$seen" 'BEGIN{printf "%.2f", 100*p/s}')" + if awk -v p="$po" -v s="$seen" 'BEGIN{exit !(100*p/s <= 10)}'; then + ok "purge-only rescues are a small fraction of flows" + else + bad "purge_only_rescued is $po of $seen flows; netifyd's contract may have shifted" + fi + else + printf ' purge_only_rescued=%s (only %s flows seen; rate not meaningful)\n' "$po" "$seen" + skip "too few flows to judge the purge-only rate" + fi +} + +# ---------------------------------------------------------------- conntrack + +test_conntrack() { + head2 "NAT TWIN RESOLUTION (conntrack)" + have_netns || { skip "needs netns '$NETNS' (conntrack)"; return; } + + case "$(st | sed -n 's/.*"available": *\(true\|false\|null\).*/\1/p' | head -1)" in + true) ok "conntrack is readable" ;; + *) bad "conntrack unavailable; NAT twins cannot be resolved" ;; + esac + + ubus call appflow reset >/dev/null 2>&1; sleep 2 + slow_pull 15 6 + sleep 20 + + local tw un + tw=$(field conntrack twins); un=$(field conntrack unresolved) + printf ' twins=%s router_local=%s unresolved=%s entries=%s\n' \ + "$tw" "$(field conntrack router_local)" "$un" "$(field conntrack entries)" + + # The first version of this fix cached the snapshot for 2 s, so new flows + # almost always missed and it silently degraded to the pre-fix behaviour + # while reporting conntrack as available. High unresolved against zero twins + # is exactly that signature. + if [ "${tw:-0}" -gt 0 ] 2>/dev/null; then + ok "NAT twins are being resolved on evidence" + elif [ "${un:-0}" -gt 0 ] 2>/dev/null; then + bad "unresolved=$un with twins=0: resolution is degrading to the safe default" + else + skip "no external captures in this window; nothing to resolve" + fi +} + +# ---------------------------------------------------------------- restart + +test_restart() { + head2 "RESTART MID-TRANSFER (reshadow must not double count)" + have_netns || { skip "needs netns '$NETNS' (restart)"; return; } + + # reshadow() used to return early on dev_key == "router", which is exactly + # how a WAN-side capture of a NAT-ed client flow is classified, so after a + # restart both copies of a flow stayed counted for its whole remaining life. + # + # READ THIS BEFORE TRUSTING A GREEN RESULT HERE. This check is a broad + # regression guard, not a reliable detector of that defect. Measured against + # the pre-fix v1.0.1 daemon: 0.701 and 0.791 on two runs on 2026-08-24, then + # 1.012 on 2026-08-25 -- the same broken code passing, because whether the + # defect manifests depends on a NAT-ed flow actually spanning the restart + # and being captured on both interfaces, and a sixty-second window does not + # guarantee that. + # + # So: a RED result here is strong evidence of a real problem. A GREEN result + # is weak evidence of correctness, and one green run is not proof the + # attribution is sound. The conservation counters above are the dependable + # signal; this one catches gross breakage. + # PRECONDITION, CHECKED RATHER THAN ASSUMED: netifyd must actually capture + # the path this traffic takes. `wire()` reads the test client's own veth, so + # it counts every byte the client sent regardless of which uplink carried + # them, while appflow can only ever count what netifyd was told to watch. + # + # This bit on 2026-08-27. A bench router had a second uplink and a VPN + # tunnel added to it while netifyd still ran `-I br-lan -E wan`, and the + # ratio dropped to 0.62. That reads as an attribution defect and is not one: + # the traffic left by a path netifyd was never watching. The unmodified + # shipped daemon produced the same failure, which is the only reason it was + # not chased as a regression. + # + # A ratio computed over an uncaptured path is not a weak signal, it is a + # meaningless one, so this skips loudly instead of reporting a number. + local caps uncap + caps=$(ps w 2>/dev/null | sed -n 's/.*netifyd.*/&/p' | head -1) + uncap="" + for d in $(ip -4 route show default 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | sort -u); do + case " $caps " in + *" $d "*) ;; + *) uncap="$uncap $d" ;; + esac + done + if [ -n "$uncap" ]; then + skip "netifyd does not capture default-route interface(s):$uncap -- ratio would be meaningless" + [ "$(field bytes leaked)" = "0" ] && ok "leaked still 0" || bad "leaked != 0" + return 0 + fi + + ubus call appflow reset >/dev/null 2>&1; sleep 2 + local w0; w0=$(wire) + + slow_pull 45 6 & + local job=$! + sleep 12 + /etc/init.d/appflowd restart >/dev/null 2>&1 + sleep 40 + wait $job 2>/dev/null + sleep 20 + + local w1 moved tot r + w1=$(wire); moved=$(( w1 - w0 )) + tot=$(st | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + [ -z "$tot" ] && tot=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + r=$(ratio "${tot:-0}" "$moved") + printf ' wire=%s appflow=%s ratio=%s\n' "$moved" "${tot:-0}" "$r" + + if [ "$moved" -lt 100000 ]; then + skip "not enough traffic moved to judge the ratio" + elif within "$r" 0.80 1.25; then + ok "total stays near wire truth across a restart (ratio $r)" + else + bad "ratio $r is outside 0.80-1.25; a restart is mis-attributing" + fi + + [ "$(field bytes leaked)" = "0" ] && ok "leaked still 0 after a restart" \ + || bad "leaked != 0 after a restart" +} + +# ---------------------------------------------------------------- acl + +test_categories() { + head2 "Every category this box's netifyd can emit has a translatable label" + + # THE DRIFT GAP, and it is the one thing the offline suite structurally + # cannot close. frontend-suite.js checks CATEGORY_LABELS against a list of + # 43 tags hand-copied from netifyd 4.4.7. That catches a deleted entry. It + # cannot catch netifyd ADDING a tag, because a list written today does not + # know about it, and the product's response to an unknown tag is to + # title-case it into credible English that is untranslatable for ever. + # + # Only a real box knows what its own netifyd carries. This check reads the + # installed category file and asserts the frontend has a label for every + # tag in it, so a netifyd upgrade that adds a category becomes a visible + # test failure rather than a silently English label. + # + # SABOTAGE: delete any entry from CATEGORY_LABELS in common.js. This goes + # red and names the tag. To see it fire for real, add a tag to + # /etc/netify.d/netify-categories.json that the frontend does not know. + local CATS=/etc/netify.d/netify-categories.json + local JS=/www/luci-static/resources/view/appflow/common.js + + [ -f "$CATS" ] || { skip "netify category file absent ($CATS)"; return; } + [ -f "$JS" ] || { skip "appflow frontend not installed ($JS)"; return; } + + local tags missing n + tags=$(jsonfilter -i "$CATS" -e '@.application_tag_index' \ + -e '@.protocol_tag_index' 2>/dev/null | + grep -o '"[a-z][a-z0-9-]*"' | tr -d '"' | sort -u) + + if [ -z "$tags" ]; then + skip "could not read any category tag out of $CATS" + return + fi + + n=$(printf '%s\n' "$tags" | grep -c .) + missing="" + for t in $tags; do + # The label table is `'tag': _('Label'),`. Matching the quoted key + # against the file is enough: an entry that exists but is not wrapped + # in _() is the offline suite's job, and it checks exactly that. + grep -q "'$t':" "$JS" || missing="$missing $t" + done + + if [ -z "$missing" ]; then + ok "all $n category tags on this box have a CATEGORY_LABELS entry" + else + bad "netifyd carries category tags the frontend cannot translate:$missing" + fi +} + +test_acl() { + head2 "ACL grants no more than the package uses" + A=/usr/share/rpcd/acl.d/luci-app-appflow.json + [ -f "$A" ] || { bad "ACL file not installed"; return; } + + # /etc/config/network holds PPPoE and 802.1x credentials and wireguard keys. + # A sibling package had this grant removed on 2026-08-25 for that reason. + if grep -q '"network"' "$A"; then + bad "ACL grants uci read on 'network' (PPPoE/wireguard secrets)" + else + ok "ACL does not grant uci read on 'network'" + fi + + # appflow reads nothing but its own ubus object, so a uci grant of any kind + # would be a grant it does not use. + if grep -q '"uci"' "$A"; then + bad "ACL contains a uci grant; this package only uses its own ubus object" + else + ok "ACL contains no uci grant at all" + fi + + if grep -q '"file"' "$A"; then + bad "ACL contains a file grant; this package shells out to nothing" + else + ok "ACL contains no file/exec grant" + fi + + # There IS a legitimate write: `reset` clears the live counters and the UI + # offers it as a button. What must not appear is a write to anything else. + W=$(sed -n '/"write"/,$p' "$A" | grep -oE '"[a-z_]+"' | grep -vE '"write"|"ubus"|"appflow"' | tr -d '"' | tr '\n' ' ') + if [ "$(echo "$W" | tr -d ' ')" = "reset" ]; then + ok "the only write grant is appflow.reset" + else + bad "write grants beyond appflow.reset: $W" + fi + + # The grant list must not exceed what the frontend actually binds. Checking + # only for absent CATEGORIES missed this: the ACL granted `apps` and `flows` + # that no view calls, and `flows` is the live per-connection table rather + # than an aggregate. The code comment even said the two were deliberately + # unbound, so the evidence was sitting next to the defect. + V=/www/luci-static/resources/view/appflow/common.js + if [ -r "$V" ]; then + DECL=$(grep -oE "declare\('[a-z_]+'" "$V" | sed "s/declare('//;s/'//" | sort -u) + GRANT=$(sed -n '/"read"/,/"write"/p' "$A" | grep -oE '"[a-z_]+"' \ + | grep -vE '"read"|"write"|"ubus"|"appflow"' | tr -d '"' | sort -u) + EXTRA=$(for g in $GRANT; do echo "$DECL" | grep -qx "$g" || echo "$g"; done | tr '\n' ' ') + if [ -z "$(echo "$EXTRA" | tr -d ' ')" ]; then + ok "every granted read method is one the frontend declares" + else + bad "granted but never called by any view: $EXTRA" + fi + else + skip "frontend not installed; cannot compare grants against declares" + fi +} + +# ---------------------------------------------------------------- run + +printf 'appflow hardware suite\n' +printf 'group: %s netns: %s veth: %s\n' "$GROUP" "$NETNS" "$VETH" +have_netns || printf 'note: netns/veth absent; traffic tests will skip\n' + +case "$GROUP" in + health) test_health ;; + conservation) test_conservation ;; + conntrack) test_conntrack ;; + restart) test_restart ;; + acl) test_acl ;; + categories) test_categories ;; + all) test_health; test_conservation; test_conntrack + test_restart; test_acl; test_categories ;; + *) printf "unknown group '%s'\n" "$GROUP"; exit 2 ;; +esac + +printf '\n----------------------------------------\n' +printf 'passed %d, failed %d, skipped %d\n' "$PASS" "$FAIL" "$SKIP" +exit $FAIL diff --git a/luci-app-appflow/tests/luci-module.js b/luci-app-appflow/tests/luci-module.js new file mode 100644 index 000000000..3d633ea2d --- /dev/null +++ b/luci-app-appflow/tests/luci-module.js @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2026 VolanticSystems +// +// Load a real LuCI view module under Node, unmodified, so its exported pure +// functions can be tested without a browser. +// +// WHY THIS EXISTS. Every browser-side defect this package has had was found by +// a human or a review panel reading the code. Not one was found by a test, +// because no test could run any of it: LuCI view files are not CommonJS or ES +// modules, they are evaluated by LuCI's own loader with the `'require x';` +// directives intercepted. So the JavaScript half of a package whose entire +// safety promise lives in `audit()` had zero automated coverage. +// +// WHAT IS AND IS NOT STUBBED, because this is where a harness like this goes +// wrong. The stubs supply DATA and inert plumbing only: +// +// baseclass.extend(o) -> returns o. LuCI returns a class whose singleton +// exposes the same properties; callers use it as +// `gen.audit()`, so a plain object is behaviourally +// identical for these tests. +// uci.sections(pkg) -> returns the fixture array the test supplied. +// rpc / request -> throw if called. Nothing under test calls them, +// and a stub that silently returned a plausible +// value would be the reimplementation trap: the +// test would pass against a product that had +// stopped asking. +// +// NO LOGIC FROM THE MODULE IS COPIED HERE. The classification, generation and +// validation code that runs is the shipped code, byte for byte. + +'use strict'; + +const fs = require('fs'); +const vm = require('vm'); +const path = require('path'); + +// Strip the LuCI loader directives. They are bare string-expression statements +// at the top of the file, exactly `'require name';` or `'require name as x';`, +// and they are the only thing preventing the file from evaluating as ordinary +// JavaScript. Replaced with a blank line each so every reported line number +// still matches the real file. +// The same String.prototype.format, as SOURCE TEXT, so it can be evaluated +// inside the vm realm where the module actually runs. +const FORMAT_SRC = ` +Object.defineProperty(String.prototype, 'format', { + value: function () { + var args = arguments, i = 0; + return this.replace(/%(?:(\d+)\$)?([sdifj%])/g, function (m, pos, kind) { + if (kind === '%') return '%'; + var v = args[pos ? (parseInt(pos, 10) - 1) : i++]; + if (kind === 'd' || kind === 'i') return String(parseInt(v, 10)); + if (kind === 'f') return String(parseFloat(v)); + if (kind === 'j') return JSON.stringify(v); + return (v == null) ? '' : String(v); + }); + } +}); +`; + +function stripRequires(src) { + return src.replace(/^\s*'require [^']*';\s*$/gm, ''); +} + +// load(file, stubs, opts) -> the object the module returns. +// +// opts.translate replaces the _() implementation. Default is identity, which +// is LuCI's real behaviour with no catalogue loaded. +// +// WHY A NON-IDENTITY TRANSLATOR IS WORTH HAVING. With identity, a label that +// went through _() and a label a runtime title-caser produced are the same +// string, so no assertion on the OUTPUT can tell them apart. That is not +// hypothetical: three label paths shipped untranslatable underneath a green +// run of a suite that asserted exactly those outputs. Loading the module a +// second time with a marking translator makes the two observationally +// different, and equality assertions become sound again rather than +// coincidentally true. Raised by a review panel, 2026-08-30. +function load(file, stubs, opts) { + const translate = (opts && opts.translate) || ((s) => s); + const src = fs.readFileSync(file, 'utf8'); + // Every string passed to _() while the module evaluates, in order. This is + // not decoration: it lets a test assert that the shipped catalogue + // (po/templates/*.pot) actually covers the strings the code asks for, which + // is otherwise a claim nobody checks until a translator files a bug. + const translated = []; + + const sandbox = Object.assign({ + baseclass: { extend: (o) => o }, + // LuCI's _() returns its msgid unchanged when no catalogue is loaded, + // which is the default English case, so identity is the real + // behaviour rather than a convenient stand-in. + _: (s) => { translated.push(s); return translate(s); }, + N_: (n, s, p) => { + translated.push(s); translated.push(p); + return translate(n === 1 ? s : p); + }, + L: { + // L.resource() builds a static asset URL. It is a pure string + // join in LuCI and is used at module top level, so it has to + // return something or the file will not evaluate. This is the one + // stub that returns a value rather than throwing, and it returns + // an obviously-fake path so a test that accidentally depends on it + // fails visibly rather than plausibly. + resource: (...p) => '/TEST-RESOURCE/' + p.join('/'), + + // L.toArray is a LuCI RUNTIME helper, not product logic, and this + // is a faithful copy of it from luci-base's luci.js rather than an + // approximation. Getting it wrong would make normalize() behave + // differently here than in a browser, which is the one way a + // harness like this lies convincingly: the product code would be + // real and the answer would still be wrong. + // + // null/undefined -> [] + // array -> itself + // object -> [ it ] + // '' -> [] + // otherwise -> String(val).trim().split(/\s+/) + toArray: (val) => { + if (val == null) return []; + if (Array.isArray(val)) return val; + if (typeof val === 'object') return [ val ]; + const s = String(val).trim(); + return s === '' ? [] : s.split(/\s+/); + }, + + // Everything else throws rather than returning something + // plausible. A stub that silently answers is how a test starts + // passing against a product that has stopped asking. + error: (e) => { throw new Error('L.error: ' + e); }, + raise: (e) => { throw new Error('L.raise: ' + e); } + }, + // LuCI's DOM element factory. The real one returns a live DOM node; + // this returns an inspectable plain tree of the same shape. + // + // BE HONEST ABOUT WHAT THIS CAN AND CANNOT TEST. It shows which + // element, class and text the code CHOSE to emit, which is exactly + // where this project's browser defects have lived: a status pill that + // rendered "healthy" for an unknown state, a tile whose colour came + // back undefined. It says nothing about layout, CSS cascade, or what + // a browser finally paints. Those still need a browser. + E: function (tag, attrs, children) { + if (attrs != null && (typeof attrs !== 'object' || Array.isArray(attrs))) { + children = attrs; + attrs = {}; + } + return { + tag: tag, + attrs: attrs || {}, + children: children == null ? [] : [].concat(children) + }; + }, + console: console + }, stubs || {}); + + // The module body ends in a top-level `return`, which is only legal inside + // a function, so wrap it in one. + const fn = new vm.Script( + '(function(){\n' + stripRequires(src) + '\n})()', + { filename: path.basename(file) } + ); + // vm gives the module its own REALM, with its own String.prototype, so a + // polyfill installed in Node's realm does not reach it. Build the context + // first, install the LuCI runtime extensions INSIDE it, then evaluate. + // Without this, product code calling '%s'.format() throws here while + // working perfectly in a browser. + const ctx = vm.createContext(sandbox); + new vm.Script(FORMAT_SRC, { filename: 'luci-runtime-shim.js' }).runInContext(ctx); + const mod = fn.runInContext(ctx); + + // Non-enumerable so it cannot be mistaken for part of the module's own + // export surface by a test that iterates the keys. + Object.defineProperty(mod, '__translated', { + value: translated, enumerable: false + }); + return mod; +} + +// Every msgid in a .pot file. Used to assert the catalogue covers the strings +// the code actually asks for. +function potMsgids(file) { + const out = new Set(); + const re = /^msgid "((?:[^"\\]|\\.)*)"/gm; + const src = fs.readFileSync(file, 'utf8'); + let m; + while ((m = re.exec(src)) !== null) { + const s = m[1].replace(/\\"/g, '"').replace(/\\\\/g, '\\') + .replace(/\\n/g, '\n').replace(/\\t/g, '\t'); + if (s) out.add(s); + } + return out; +} + +// A uci stub whose sections() returns fixture data in LuCI's own shape: +// each section is a flat object of its options plus '.name' and '.type'. +function uciStub(sectionsByPackage) { + return { + sections: (pkg) => (sectionsByPackage[pkg] || []), + get: (pkg, sec, opt) => { + const list = sectionsByPackage[pkg] || []; + const s = list.find((x) => x['.name'] === sec); + return s ? s[opt] : undefined; + } + }; +} + +// Parse the output of `uci show ` into LuCI section objects. +// +// This is here so a fixture can be written in the SAME text the init script +// parses, rather than as hand-built objects that have quietly drifted from +// what uci actually emits. The two halves of the ownership rule are supposed +// to see the same configuration; letting them be described twice, in two +// notations, is how they stopped agreeing in the first place. +function parseUciShow(text) { + const sections = []; + const byName = {}; + for (const line of text.split('\n')) { + const t = line.trim(); + if (!t) continue; + let m = t.match(/^([a-z0-9_-]+)\.([^.=]+)=(\S+)$/i); + if (m) { + const sec = { '.name': m[2], '.type': m[3] }; + byName[m[2]] = sec; + sections.push(sec); + continue; + } + m = t.match(/^([a-z0-9_-]+)\.([^.=]+)\.([^=]+)='(.*)'$/i); + if (m && byName[m[2]]) byName[m[2]][m[3]] = m[4]; + } + return sections; +} + +// Flatten an E()-tree to its visible text, and to the set of class names used. +// Both are what a test wants to assert on: "which pill did it choose", not +// "what did the browser paint". +function textOf(node) { + if (node == null || node === false) return ''; + if (typeof node === 'string' || typeof node === 'number') return String(node); + if (Array.isArray(node)) return node.map(textOf).join(''); + if (node.children) return node.children.map(textOf).join(''); + return ''; +} + +function classesOf(node, out) { + out = out || new Set(); + if (node == null || typeof node !== 'object') return out; + if (Array.isArray(node)) { node.forEach((n) => classesOf(n, out)); return out; } + const c = node.attrs && (node.attrs['class'] || node.attrs.class); + if (typeof c === 'string') c.trim().split(/\s+/).forEach((x) => x && out.add(x)); + (node.children || []).forEach((n) => classesOf(n, out)); + return out; +} + +module.exports = { load, uciStub, parseUciShow, stripRequires, potMsgids, textOf, classesOf }; diff --git a/luci-app-appflow/tests/protocol-suite.sh b/luci-app-appflow/tests/protocol-suite.sh new file mode 100755 index 000000000..c35a4a465 --- /dev/null +++ b/luci-app-appflow/tests/protocol-suite.sh @@ -0,0 +1,1873 @@ +#!/bin/sh +# appflow protocol and bounds suite. +# +# The sibling suite, hardware-suite.sh, drives real traffic through a real +# netifyd and checks that the totals stay near wire truth. It is an integration +# test and its own header says the weakness is coverage: it can only assert on +# whatever netifyd happened to send during the window, so the arithmetic is +# checked loosely, the error paths are never reached at all, and a test either +# waits a minute or skips. +# +# This suite replaces netifyd with a socket WE control, so the input is exact +# and chosen. That buys three things the hardware suite cannot have: +# +# 1. Byte arithmetic checked against a hand-computed total rather than +# against a ratio band. If the daemon adds one byte that netifyd did not +# report, a check here goes red; the 0.80-1.25 ratio band cannot see it. +# 2. The error and bounds paths, which real netifyd never exercises: +# malformed frames, oversized tags, a full flow table, a full aggregate +# table, unknown event types, a peer that stops sending newlines. +# 3. Determinism. No traffic, no waiting on a transfer, no SKIP. +# +# It does NOT replace the hardware suite. Everything here is the daemon's view +# of a wire this file wrote, so nothing here can catch appflowd and netifyd +# disagreeing about what a field means. That is exactly what the hardware +# suite's wire-truth comparison is for, and neither suite subsumes the other. +# +# usage: sh protocol-suite.sh run everything +# sh protocol-suite.sh conservation run one group +# (conservation|protocol|bounds|attribution|hostile|acl|lifecycle) +# +# RUN IT ON A SANDBOX ROUTER. It stops appflowd, repoints appflow.socket_path +# at a temporary socket, and restarts the daemon many times. It restores the +# original socket_path on exit, including on interrupt. It never touches +# netifyd's own configuration. +# +# Exit status is the number of failed checks. +# +# --------------------------------------------------------------------------- +# HOW TO READ THE `SABOTAGE:` COMMENTS, AND WHY THEY ARE THERE +# +# Every check below carries one. It names the smallest edit to appflowd -- the +# product, not this file -- that makes that check report failure, and it was +# written BEFORE the assertion it sits above. +# +# That ordering is the whole discipline. Asking "what does this test check" +# answers itself in the author's own words, which is how a test that cannot +# fail gets written and then read a dozen times without anyone noticing. Asking +# "what edit turns this red" is a construction task: either a concrete edit +# exists or you hit a wall, and the wall is the finding. +# +# They are also here so a reviewer can check this suite in sixty seconds rather +# than trusting it. Apply the named edit, run the group, and exactly the checks +# that name it should go red. If one of them stays green, that check is +# decorative and this comment is how you found out. +# +# An edit that makes the file unparseable does not count and none is used +# below: an error is not a red assertion, and it is the escape hatch every +# hollow test reaches for. +# --------------------------------------------------------------------------- + +set -u + +# ---------------------------------------------------------------- exclusivity +# +# Every suite in these two packages mutates GLOBAL router state: this one +# rewrites /etc/config/mwan3 and drives the mwan3 service, its sibling repoints +# appflow.socket_path and restarts appflowd. Two of them running at once +# corrupt each other's fixtures, and the failures look like product defects. +# +# That is not hypothetical. On 2026-08-26 two suites were launched a minute +# apart against the same router; the second reported two failures that could +# not be reproduced in isolation, and the ownership arithmetic was rewritten +# twice chasing a bug that was never there. +# +# One lock file for ALL suites across both packages, because the resource being +# protected is the router, not the config file. +# mkdir is atomic and is NOT a file descriptor, which is the whole point. +# The first version of this guard used `exec 9>lock; flock -n 9`. Every child +# inherits an fd, and these suites launch children that outlive them: socat +# serving the fake agent, and mwan3track respawned per interface by mwan3's +# init script. The lock therefore stayed held after the suite exited and the +# next suite in a serial run was refused. Observed: suite 1 passed, suites 2 +# and 3 produced no output at all because both exited 2. +SUITE_LOCK=/tmp/openwrt-suite.lock.d +if ! mkdir "$SUITE_LOCK" 2>/dev/null; then + # Someone holds it, or a killed run left it behind. Only the second is + # ours to clear, and only when the recorded pid is provably gone. + stale=1 + if [ -r "$SUITE_LOCK/pid" ]; then + kill -0 "$(cat "$SUITE_LOCK/pid" 2>/dev/null)" 2>/dev/null && stale=0 + fi + if [ "$stale" = 1 ]; then + printf 'clearing a stale suite lock (%s)\n' "$SUITE_LOCK" + rm -rf "$SUITE_LOCK" + mkdir "$SUITE_LOCK" 2>/dev/null || { printf "cannot take the suite lock\n"; exit 2; } + else + printf 'another test suite is running on this router (pid %s).\n' \ + "$(cat "$SUITE_LOCK/pid" 2>/dev/null)" + printf 'they mutate shared router state; run them one at a time.\n' + exit 2 + fi +fi +echo $$ > "$SUITE_LOCK/pid" + +GROUP="${1:-all}" +PASS=0; FAIL=0 + +SOCK=/tmp/appflow-proto.sock +EV=/tmp/appflow-proto-events.jsonl +FAKE="" +ORIG_SOCK="" +ORIG_FLOWMAX="" + +head2() { printf '\n=== %s ===\n' "$*"; } +ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$*"; } +bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$*"; } +note() { printf ' %s\n' "$*"; } + +# chk +chk() { + if [ "$2" = "$3" ]; then ok "$1"; else bad "$1 (expected '$2', got '$3')"; fi +} + +# chk_ge +chk_ge() { + if [ "${3:-0}" -ge "$2" ] 2>/dev/null; then + ok "$1 ($3 >= $2)" + else + bad "$1 (expected >= $2, got '${3:-}')" + fi +} + +# ------------------------------------------------------------------ fixtures + +st() { ubus call appflow status 2>/dev/null; } + +# field -- read one integer out of one object in status. +# +# Anchored to the group so `flows.pruned` and `aggregates.pruned`, which share a +# key name, cannot be confused for each other. +# +# TWO DEFECTS IN EARLIER DRAFTS OF THIS FUNCTION, both found by running it, and +# both worth keeping written down because they are the shape that makes a whole +# suite worthless rather than one check: +# +# 1. The range ended at `/}/`, which is the first closing brace ANYWHERE in +# the group -- and `events` contains two nested objects, `unknown_types` +# and `by_type`. So the range closed early and `rx_bytes`, `resyncs` and +# `last_event_age_ms` were unreadable. Anchoring both ends to a single tab +# of indentation is what fixes it. +# +# 2. Far worse: a key that could not be found returned the empty string, and +# every caller wrote `${x:-0}`. Six checks in this suite assert that a +# counter is exactly "0". Every one of them would have passed +# unconditionally, for as long as the helper stayed broken, with the +# daemon uninstalled entirely. That is the "loop over a property that does +# not exist" trap wearing a different hat: not zero iterations, but a +# default that happens to equal the expected value. +# +# So a miss now returns the literal MISSING, which equals no expected value and +# turns the check red. A test helper that cannot find its subject must fail +# loudly; returning a plausible number is how a suite starts lying. +field() { + local v + v=$(st | sed -n "/^ \"$1\": {/,/^ }/p" \ + | sed -n "s/.*\"$2\": *\([0-9-]*\).*/\1/p" | head -1) + printf '%s' "${v:-MISSING}" +} + +cleanup() { + fake_down + rm -f "$SOCK" "$EV" + if [ -n "$ORIG_SOCK" ]; then + uci set appflow.main.socket_path="$ORIG_SOCK" + [ -n "$ORIG_FLOWMAX" ] && uci set appflow.main.flow_max="$ORIG_FLOWMAX" + uci commit appflow + /etc/init.d/appflowd restart >/dev/null 2>&1 + printf '\nrestored appflow.socket_path=%s flow_max=%s\n' "$ORIG_SOCK" "$ORIG_FLOWMAX" + fi + rm -rf "$SUITE_LOCK" +} +# HUP IS IN THAT LIST DELIBERATELY. These suites are normally run over SSH, and +# a dropped session sends SIGHUP, which the original INT/TERM/EXIT list did not +# catch: the process died without running the trap. Observed 2026-08-26, where +# it left a stale lock behind. The lock self-heals; what would NOT self-heal is +# the rest of what this trap undoes -- the mwan3 init-script shim, a rewritten +# /etc/config/mwan3, or appflow left pointed at a socket that no longer exists. +# A router in that state looks broken and gives no clue why. +trap 'cleanup; exit $FAIL' INT TERM HUP EXIT + +# Start a fake netifyd that serves $EV and then holds the connection open. +# +# It must HOLD rather than close. appflowd treats a closed socket as netifyd +# going away and reconnects after a second, and socat is in `fork` mode, so the +# reconnect would be served a FRESH `cat` of the same file: every event +# delivered twice, every byte counted twice, and every arithmetic check in this +# suite quietly wrong in a way that looks like a daemon defect. The hold is +# what makes the input exactly-once. +# +# HOLD_S is 60, not 600. At 600 the shell that socat spawns outlives the suite +# holding the inherited stdout pipe, so anything reading this script's output +# blocks for ten minutes after the last check has printed. That is not +# hypothetical, it happened on the first run of this file. cleanup() now kills +# the child by pattern as well as killing socat, and the shorter hold bounds the +# damage if both miss. +HOLD_S=60 + +fake_up() { + fake_down + rm -f "$SOCK" + socat UNIX-LISTEN:"$SOCK",fork,unlink-early \ + SYSTEM:"cat $EV; sleep $HOLD_S" 2>/dev/null & + FAKE=$! + local i=0 + while [ "$i" -lt 30 ]; do + [ -S "$SOCK" ] && return 0 + i=$((i + 1)); sleep 1 + done + return 1 +} + +# kill every process whose command line matches, or nothing if none do. +# +# `pkill` DOES NOT EXIST on stock OpenWrt. busybox ships pgrep and killall but +# not pkill, so `pkill -f ...` is `ash: pkill: not found` -- which, inside a +# cleanup function whose failures are all redirected to /dev/null, is a silent +# no-op. The first version of this suite used it twice and left socat and its +# child running after every case. +# +# The `[ -n ... ]` guard matters too: bare `kill $(pgrep ...)` with no matches +# calls kill with no arguments, which is a usage error, and under `set -u`-style +# discipline that noise hides the real failures. +killmatch() { + local pids + pids=$(pgrep -f "$1" 2>/dev/null) + [ -n "$pids" ] && kill $pids 2>/dev/null + return 0 +} + +fake_down() { + [ -n "$FAKE" ] && kill "$FAKE" 2>/dev/null + killmatch "UNIX-LISTEN:$SOCK" + # The SYSTEM: child is a separate process and does not die with socat. + killmatch "cat $EV" + FAKE="" +} + +# Restart the daemon against the current $EV and let it drain. +# +# The wait is on OBSERVED PROGRESS rather than a fixed sleep: poll until the +# daemon reports having read the bytes we staged, then stop. A fixed sleep is +# either slower than it needs to be or, on a loaded box, shorter than the drain, +# and a check that reads counters mid-drain fails intermittently and gets +# "fixed" by lengthening the sleep until it no longer tests anything. +feed_and_settle() { + local want; want=$(wc -c < "$EV") + fake_up || { bad "fake netifyd did not come up"; return 1; } + /etc/init.d/appflowd restart >/dev/null 2>&1 + local i=0 rx=0 + while [ "$i" -lt 40 ]; do + rx=$(field events rx_bytes); rx=${rx:-0} + [ "$rx" -ge "$want" ] 2>/dev/null && { sleep 1; return 0; } + i=$((i + 1)); sleep 1 + done + note "warning: drained ${rx} of ${want} staged bytes before the deadline" + return 0 +} + +# A flow event. Deliberately verbose rather than templated: a generator that +# builds these from a table would put the expected byte totals and the events +# that produce them in the same expression, and then the oracle comes from the +# artifact under test rather than from a human. The numbers below are stated by +# hand, and the totals asserted against are stated by hand too. +# +# flow_ev +flow_ev() { + printf '{"type":"flow","internal":%s,"interface":"%s","flow":{"digest":"%s","local_ip":"192.168.9.10","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_port":40001,"other_port":443,"ip_protocol":6,"detected_application":%s,"detected_application_name":"%s","detected_protocol":91,"detected_protocol_name":"TLS","category":{"application":10,"protocol":20},"local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' \ + "$7" "$([ "$7" = true ] && echo br-lan || echo eth0)" "$1" "$8" "$2" "$3" + printf '{"type":"flow_stats","internal":%s,"interface":"%s","flow":{"digest":"%s","local_ip":"192.168.9.10","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":%s,"other_bytes":%s,"total_bytes":%s}}\n' \ + "$7" "$([ "$7" = true ] && echo br-lan || echo eth0)" "$1" "$8" "$4" "$5" "$6" +} + +hello() { printf '{"type":"agent_hello","agent_version":"test-4.4.7","build_version":"protocol-suite"}\n'; } + +# ------------------------------------------------------------- conservation + +test_conservation() { + head2 "BYTE CONSERVATION (against a hand-computed total, not a ratio band)" + + # Three flows, chosen so no two sums coincide and a swapped or dropped term + # cannot land on the right answer by accident: + # + # flow A local 1000 other 2000 total 3000 + # flow B local 500 other 700 total 1200 + # flow C local 17 other 83 total 100 + # + # sum(local) = 1517 sum(other) = 2783 sum(total) = 4300 + # + # Every expected value below is one of those three, computed here and not + # read back from the daemon. + { + hello + flow_ev A0000001 1001 protoapp-one 1000 2000 3000 true 02:00:00:00:00:aa + flow_ev B0000002 1002 protoapp-two 500 700 1200 true 02:00:00:00:00:aa + flow_ev C0000003 1003 protoapp-three 17 83 100 true 02:00:00:00:00:aa + } > "$EV" + + feed_and_settle || return + + local rep att shd lk up dn tot + rep=$(field bytes reported); att=$(field bytes attributed) + shd=$(field bytes shadowed); lk=$(field bytes leaked) + note "reported=$rep attributed=$att shadowed=$shd leaked=$lk" + + # SABOTAGE: in appflowd flow_update(), change the `stats.bytes.reported +=` + # line to add `tb` instead of the clamped delta. reported becomes 4300 on + # the first update and keeps climbing; this check goes red immediately. + chk "reported == the 4300 bytes we sent, exactly" "4300" "$rep" + + # SABOTAGE: in account(), drop the `stats.bytes.attributed +=` line. + # attributed falls to 0 while reported stays 4300, and leaked becomes 4300. + chk "attributed == reported when nothing is shadowed" "4300" "$att" + chk "nothing was shadowed on an all-internal capture" "0" "$shd" + + # SABOTAGE: same edit as above. leaked is reported minus what reached an + # aggregate, so any accounting path that stops adding shows here. This is + # the check the README tells a user to run, so it must bind. + chk "leaked == 0 (every reported byte reached an aggregate)" "0" "$lk" + + head2 "DIRECTIONAL SPLIT (a swap survives every total-only check)" + + up=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_up": *\([0-9]*\).*/\1/p' | head -1) + dn=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_down": *\([0-9]*\).*/\1/p' | head -1) + tot=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + note "up=$up down=$dn total=$tot" + + # SABOTAGE: in flow_update(), swap the two initialisers so `dl` takes + # f.other_bytes and `do_` takes f.local_bytes. + # + # This is why the two directions are asserted separately rather than as a + # sum. That swap leaves reported, attributed, leaked and bytes_total all + # exactly correct -- every check in the group above stays green, and so does + # the hardware suite's wire-truth ratio, because the sum is unchanged. Only + # these two go red. A suite that checks totals alone cannot see a swap. + chk "bytes_up == sum(local_bytes) == 1517" "1517" "$up" + chk "bytes_down == sum(other_bytes) == 2783" "2783" "$dn" + chk "bytes_total == 4300" "4300" "$tot" + + head2 "SELF-INCONSISTENT COUNTERS ARE CLAMPED ONCE A BASELINE EXISTS" + + # netifyd is trusted to report its own counters honestly, and DESIGN 2.2 + # says the directional deltas are bounded by the growth of total_bytes so a + # self-inconsistent counter cannot inflate the totals. + # + # READ THE GUARD BEFORE BELIEVING THAT SENTENCE. In flow_update() the clamp + # sits behind `if (tb > 0 && fr.total > 0)`, so it applies from the SECOND + # update of a flow onward. On the first there is no baseline to measure + # growth against and the deltas are trusted as given -- deliberately, and + # the comment beside it argues the case: a missing or reset counter must not + # scale real deltas to zero, which would silently stop accounting a flow + # forever. That is a defensible trade and it is not what this checks. + # + # The first draft of this check sent ONE update with local 1e9 + other 1e9 + # against total 100 and asserted the result was bounded by 100. It came back + # 2000000000 and read as a live defect. It was not: the test asserted a + # promise the code never made, on the one path where the promise is + # explicitly suspended. Written sabotage-first it would have died at the + # wall, because there is no clamp on that path to remove. + # + # So drive the path the clamp actually governs. Two updates: + # + # stats #1 local 100 other 100 total 200 no baseline, trusted + # stats #2 local 1e9 other 1e9 total 250 baseline 200, grew 50 + # + # Growth is 50, so the second update may contribute 50 and no more: + # reported = 200 + 50 = 250 + # attributed = 100 + 100 + 25 + 25 = 250 + # + # Both hand-computed. An unclamped daemon reports 2000000200. + { + hello + printf '{"type":"flow","internal":true,"interface":"br-lan","flow":{"digest":"D0000004","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","detected_application":1004,"detected_application_name":"protoapp-liar","local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"D0000004","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":100,"other_bytes":100,"total_bytes":200}}\n' + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"D0000004","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":1000000000,"other_bytes":1000000000,"total_bytes":250}}\n' + } > "$EV" + feed_and_settle || return + + rep=$(field bytes reported); att=$(field bytes attributed) + note "reported=$rep attributed=$att (two billion offered, 50 bytes of growth)" + + # SABOTAGE: in flow_update(), delete the `if (sum > dt)` branch so `dl` and + # `do_` pass through unclamped. reported becomes 2000000200 and both checks + # go red. + chk "growth, not the declared deltas, bounds what is reported" "250" "$rep" + chk "and what is attributed" "250" "$att" + + # The first-update path is a real and deliberate hole in the clamp: one + # oversized update on a brand-new digest is trusted in full. Recorded here + # rather than asserted, because the daemon documents the trade and changing + # it is a design decision, not a bug fix. + note "note: the clamp does not cover a flow's FIRST update, by design" +} + +# ------------------------------------------------------------------ protocol + +test_protocol() { + head2 "WIRE PROTOCOL (the paths real netifyd never exercises)" + + # One good flow so there is something to compare against, then five lines + # that are not JSON objects at all, then another good flow. The good flows + # bracket the garbage: if a malformed line desynchronised the reader, the + # second flow would not arrive and the byte total would be short. + { + hello + flow_ev E0000005 1005 protoapp-before 100 200 300 true 02:00:00:00:00:aa + printf '{"type":"flow","flow":{"digest":"trunc\n' + printf 'not json at all\n' + printf '[1,2,3]\n' + printf '"a bare string"\n' + printf '{"unterminated": \n' + flow_ev F0000006 1006 protoapp-after 400 500 900 true 02:00:00:00:00:aa + } > "$EV" + feed_and_settle || return + + local mal rep + mal=$(field events malformed) + rep=$(field bytes reported) + note "malformed=$mal reported=$rep" + + # SABOTAGE: in feed(), delete `stats.malformed++` from the json() catch + # block. malformed stays 0 and this goes red. + # + # The floor is 4 rather than 5 because two of the six garbage lines are a + # truncated object and an unterminated one; how the split lands depends on + # buffering, and pinning the exact count would make this check assert on + # socat's write sizes rather than on the daemon. Four is below any plausible + # split and above zero, which is the property under test. + chk_ge "malformed lines are counted, not silently dropped" 4 "$mal" + + # SABOTAGE: in feed(), change the `return` in the catch block to a bare + # `dispatch(ev, now)` on an undefined `ev`. The daemon stops parsing after + # the first garbage line and this total falls to 300. + chk "a malformed line does not desynchronise the reader (300+900)" "1200" "$rep" + + head2 "FRAME HEADERS ARE NOT EVENTS, AND EVENTS ARE NOT FRAMES" + + { + hello + printf '{"length":512}\n' + printf '{"length":1024}\n' + flow_ev G0000007 1007 protoapp-framed 300 300 600 true 02:00:00:00:00:aa + } > "$EV" + feed_and_settle || return + + local fr ev_total + fr=$(field events frames) + ev_total=$(field events total) + note "frames=$fr events.total=$ev_total" + + # SABOTAGE: in feed(), delete `stats.frames++` from the frame-header branch. + chk "a sole-key {\"length\":N} object counts as a frame" "2" "$fr" + + # SABOTAGE: in feed(), weaken the frame test from + # `ev.length != null && length(ev) == 1` + # to just `ev.length != null`. + # + # This is the interesting direction and the reason the check below exists. + # With the key-count dropped, ANY event that happens to carry a `length` + # field is swallowed as a frame header and never dispatched. netifyd does + # not send one today, so nothing in the hardware suite or in production + # would notice until it did -- at which point flows would silently vanish. + { + hello + printf '{"type":"flow","length":99,"internal":true,"interface":"br-lan","flow":{"digest":"H0000008","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","detected_application":1008,"detected_application_name":"protoapp-lengthy","local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' + printf '{"type":"flow_stats","length":99,"internal":true,"interface":"br-lan","flow":{"digest":"H0000008","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":250,"other_bytes":250,"total_bytes":500}}\n' + } > "$EV" + feed_and_settle || return + + rep=$(field bytes reported) + chk "an event carrying a 'length' field is still dispatched" "500" "$rep" + + head2 "UNKNOWN EVENT TYPES ARE COUNTED AND NAMED" + + { + hello + printf '{"type":"some_future_event","payload":{"a":1}}\n' + printf '{"type":"another_new_one","payload":{"b":2}}\n' + } > "$EV" + feed_and_settle || return + + local unk + unk=$(field events unknown) + note "unknown=$unk" + + # SABOTAGE: in dispatch(), delete the `stats.unknown++` line at the tail. + chk_ge "an unhandled event type is counted" 2 "$unk" + + # SABOTAGE: in dispatch(), delete the `stats.unknown_types[t] = 0` + # initialiser so the name is never recorded. + if st | grep -q '"some_future_event"'; then + ok "the unknown type is named in status, not just counted" + else + bad "unknown_types does not name 'some_future_event'" + fi + + head2 "A PURGE-ISH UNKNOWN TYPE STILL FINALISES THE FLOW" + + # PURGE_RE exists so a netifyd rename cannot leak flow records. Nothing in + # the hardware suite reaches it, because real netifyd only ever sends the + # three names the handler table already knows. + { + hello + flow_ev I0000009 1009 protoapp-terminated 100 100 200 true 02:00:00:00:00:aa + printf '{"type":"flow_terminated","internal":true,"interface":"br-lan","flow":{"digest":"I0000009","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":0,"other_bytes":0,"total_bytes":200}}\n' + } > "$EV" + feed_and_settle || return + + local tracked purged + tracked=$(field flows tracked); purged=$(field flows purged) + note "tracked=$tracked purged=$purged" + + # SABOTAGE: in dispatch(), delete the `if (match(t, PURGE_RE))` branch. + # The flow is never finalised, so tracked stays 1 and purged stays 0. + chk "a 'flow_terminated' event purges the flow" "0" "$tracked" + chk_ge "the purge was counted" 1 "$purged" + + head2 "BLANK LINES ARE IGNORED, NOT COUNTED AS MALFORMED" + + { + hello + printf '\n\n \n\t\n' + flow_ev J0000010 1010 protoapp-blanks 50 50 100 true 02:00:00:00:00:aa + } > "$EV" + feed_and_settle || return + + mal=$(field events malformed) + + # SABOTAGE: in feed(), delete the `if (!length(line)) return;` guard. + # Each blank line then reaches json(), throws, and is counted, so malformed + # becomes 4 and this goes red. + chk "whitespace-only lines are not counted as malformed" "0" "$mal" +} + +# -------------------------------------------------------------------- bounds + +test_bounds() { + head2 "BOUNDS: a single flow record cannot hold an unbounded string" + + # TAG_MAX is 64. Send an 8 KB application name. + local longname + longname=$(awk 'BEGIN{ s=""; while (length(s) < 8192) s = s "A"; print s }') + { + hello + printf '{"type":"flow","internal":true,"interface":"br-lan","flow":{"digest":"K0000011","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","detected_application":1011,"detected_application_name":"%s","local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' "$longname" + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"K0000011","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":10,"other_bytes":10,"total_bytes":20}}\n' + } > "$EV" + feed_and_settle || return + + # Longest run of A's anywhere in the summary reply. + local worst + worst=$(ubus call appflow summary 2>/dev/null | grep -oE 'A+' | awk '{ if (length($0) > m) m = length($0) } END { print m+0 }') + note "longest A-run in the summary reply: $worst (TAG_MAX is 64)" + + # SABOTAGE: in flow_identify(), replace the `cap(...)` wrapper around + # f.detected_application_name with the raw field. The stored tag becomes + # 8192 characters and this goes red. + if [ "${worst:-0}" -le 64 ] 2>/dev/null && [ "${worst:-0}" -gt 0 ]; then + ok "an oversized detection tag is capped at TAG_MAX ($worst <= 64)" + else + bad "detection tag not capped: longest run is $worst" + fi + + head2 "BOUNDS: the flow table refuses to grow past flow_max" + + # flow_max is clamped to a floor of 64 by cfg_load, so 64 is the smallest + # table this can ask for. 200 distinct digests against it. + uci set appflow.main.flow_max=64 + uci commit appflow + { + hello + awk 'BEGIN { + for (i = 1; i <= 200; i++) { + d = sprintf("FLOOD%05d", i) + printf "{\"type\":\"flow\",\"internal\":true,\"interface\":\"br-lan\",\"flow\":{\"digest\":\"%s\",\"local_ip\":\"192.168.9.10\",\"local_mac\":\"02:00:00:00:00:aa\",\"other_ip\":\"93.184.216.34\",\"other_mac\":\"02:00:00:00:00:ff\",\"detected_application\":2000,\"detected_application_name\":\"flood\",\"local_bytes\":0,\"other_bytes\":0,\"total_bytes\":0}}\n", d + printf "{\"type\":\"flow_stats\",\"internal\":true,\"interface\":\"br-lan\",\"flow\":{\"digest\":\"%s\",\"local_ip\":\"192.168.9.10\",\"local_mac\":\"02:00:00:00:00:aa\",\"other_ip\":\"93.184.216.34\",\"other_mac\":\"02:00:00:00:00:ff\",\"local_bytes\":1,\"other_bytes\":1,\"total_bytes\":2}}\n", d + } + }' + } > "$EV" + feed_and_settle || return + + local tracked dropped pruned + tracked=$(field flows tracked); dropped=$(field flows dropped) + pruned=$(field flows pruned) + note "tracked=$tracked pruned=$pruned dropped=$dropped against flow_max=64" + + # SABOTAGE: in flow_new(), delete the `if (nflows >= cfg.flow_max)` block. + # tracked climbs to 200 and this goes red. + if [ "${tracked:-0}" -le 64 ] 2>/dev/null; then + ok "the flow table stays within flow_max ($tracked <= 64)" + else + bad "flow table holds $tracked with flow_max=64" + fi + + # The bound above passes trivially if the table never actually filled, so + # prove the cap-pressure path RAN. Without this, offering three flows + # instead of two hundred would look identical. + # + # SABOTAGE: in prune(), delete the `if (force && nflows >= cfg.flow_max)` + # block. Nothing sheds, tracked climbs past 64 and pruned stays 0, so this + # and the check above go red together. + chk_ge "the cap-pressure prune actually ran" 1 "$pruned" + + # `flows.shed` is the counter that says "the table is too small for this + # network", and it exists because of this check. + # + # The first version asserted `dropped >= 1` and failed. That was the test + # being wrong, not the daemon: flow_new() increments flows_dropped only if + # the table is STILL full after prune(now, true), and that prune + # unconditionally sheds `int(flow_max/10) + 1` entries, so it always frees + # space and the fall-through never happens. `flows_dropped` cannot be moved + # by any input this daemon can receive. + # + # Status therefore carried a counter that permanently read 0 while flows + # were being shed continuously, with the shedding hidden inside `pruned` + # alongside ordinary idle housekeeping. Same shape as aggregates.refused. + # `flows.shed` was added 2026-08-26 to make it visible. + # + # SABOTAGE: in prune(), delete the `stats.flows_shed++` line. The table + # still sheds correctly and the operator still cannot see it, which is + # exactly the defect this counter was added for. + local shed; shed=$(field flows shed) + note "flows.shed=$shed flows.dropped=$dropped (dropped is a last-resort backstop)" + chk_ge "cap-pressure shedding is counted separately from idle pruning" 1 "$shed" + + uci set appflow.main.flow_max="$ORIG_FLOWMAX" + uci commit appflow + + head2 "BOUNDS: a full aggregate table refuses rather than evicting live data" + + # AGG_MAX is 384 per class. 500 distinct application ids, each with a live + # flow, so nothing is evictable: every entry has cur.flows > 0. + # + # This is the LAN-attacker case the 2026-08-25 security panel converged on, + # reproduced deterministically. The right behaviour is the one already + # shipped: refuse to grow rather than drop a device that is actively + # transferring. What this checks is that the refusal is VISIBLE, because a + # status reading `apps: 384` with no other signal looks healthy at exactly + # the moment the table has stopped accepting anything. + { + hello + awk 'BEGIN { + for (i = 1; i <= 500; i++) { + d = sprintf("AGGF%06d", i) + printf "{\"type\":\"flow\",\"internal\":true,\"interface\":\"br-lan\",\"flow\":{\"digest\":\"%s\",\"local_ip\":\"192.168.9.10\",\"local_mac\":\"02:00:00:00:00:aa\",\"other_ip\":\"93.184.216.34\",\"other_mac\":\"02:00:00:00:00:ff\",\"detected_application\":%d,\"detected_application_name\":\"agg-%d\",\"local_bytes\":0,\"other_bytes\":0,\"total_bytes\":0}}\n", d, 5000+i, i + printf "{\"type\":\"flow_stats\",\"internal\":true,\"interface\":\"br-lan\",\"flow\":{\"digest\":\"%s\",\"local_ip\":\"192.168.9.10\",\"local_mac\":\"02:00:00:00:00:aa\",\"other_ip\":\"93.184.216.34\",\"other_mac\":\"02:00:00:00:00:ff\",\"local_bytes\":10,\"other_bytes\":10,\"total_bytes\":20}}\n", d + } + }' + } > "$EV" + feed_and_settle || return + + local apps refused rep2 + apps=$(field aggregates apps); refused=$(field aggregates refused) + rep2=$(field bytes reported) + note "apps=$apps refused=$refused reported=$rep2 (500 apps offered, AGG_MAX is 384)" + + # SABOTAGE: in agg_make(), delete `stats.aggs_refused++` from the + # victim == null branch. The table still refuses correctly and the operator + # still cannot see it, which is the defect this counter was added for. + chk_ge "a refused aggregate is counted, not only logged" 1 "$refused" + + if [ "${apps:-0}" -le 384 ] 2>/dev/null; then + ok "the aggregate table stays within AGG_MAX ($apps <= 384)" + else + bad "aggregate table holds $apps, above AGG_MAX" + fi + + # SABOTAGE: in agg_make(), change the victim == null branch to evict the + # oldest entry regardless of cur.flows. Bytes belonging to an evicted live + # aggregate stop reaching the totals, so reported and the summary total + # diverge and this goes red. + # + # 500 flows x 20 bytes = 10000, and every one is reported whether or not it + # got its own aggregate row. That is the documented promise: the table + # refuses to GROW, it does not drop bytes from the totals. + chk "every offered byte is still reported when the table is full" "10000" "$rep2" + + head2 "BOUNDS: the unknown-type map cannot be grown without limit" + + # UNKNOWN_TYPE_MAX is 16. Offer 60 distinct unhandled type names. + { + hello + awk 'BEGIN { for (i = 1; i <= 60; i++) printf "{\"type\":\"unkflood_%d\",\"payload\":{}}\n", i }' + } > "$EV" + feed_and_settle || return + + local ntypes + ntypes=$(st | sed -n '/"unknown_types": {/,/}/p' | grep -c '": *[0-9]') + note "distinct unknown types remembered: $ntypes (UNKNOWN_TYPE_MAX is 16)" + + # SABOTAGE: in dispatch(), delete the + # `length(stats.unknown_types) < UNKNOWN_TYPE_MAX` guard. The map grows to + # 60 and this goes red. A peer that can pick type names would otherwise grow + # it without bound, and it is serialised into every status reply. + if [ "${ntypes:-0}" -le 16 ] 2>/dev/null; then + ok "the unknown-type map is bounded ($ntypes <= 16)" + else + bad "unknown_types holds $ntypes entries" + fi + + head2 "BOUNDS: a peer that stops sending newlines is resynced, not unbounded" + + # RX_MAX is 262144. Send 512 KB with no newline in it at all, then a real + # flow, so we can prove the reader recovered rather than merely survived. + { + hello + awk 'BEGIN { s = ""; while (length(s) < 4096) s = s "x"; for (i = 0; i < 128; i++) printf "%s", s }' + printf '\n' + flow_ev L0000012 1012 protoapp-afterflood 60 40 100 true 02:00:00:00:00:aa + } > "$EV" + feed_and_settle || return + + local rs rep3 + rs=$(field events resyncs); rep3=$(field bytes reported) + note "resyncs=$rs reported=$rep3" + + # SABOTAGE: in on_readable(), delete the `if (length(rxbuf) > RX_MAX)` + # block. resyncs stays 0 and the buffer grows to whatever the peer sends. + chk_ge "a 512 KB line without a newline forces a resync" 1 "$rs" + + # SABOTAGE: in the same block, drop back to `rxbuf = ""` unconditionally + # instead of cutting at the last newline. The flow that follows the flood is + # then eaten along with it and this goes red. + chk "the reader recovers and still parses the next flow" "100" "$rep3" +} + +# --------------------------------------------------------------- attribution + +test_attribution() { + head2 "ATTRIBUTION: a WAN-side copy of a LAN flow is shadowed, not counted twice" + + # The double-count class. With netifyd watching br-lan and the WAN at once, + # a client's traffic is captured twice; DESIGN 3.2 says the internal capture + # is the device's own copy and the external one is a NAT twin. + # + # Same digest, two captures, 1000 bytes each. The honest answer is that 2000 + # were reported and 1000 attributed, with 1000 shadowed. A daemon that + # counts both says the client used twice what it did. + { + hello + flow_ev M0000013 1013 protoapp-twin 500 500 1000 true 02:00:00:00:00:aa + flow_ev N0000014 1013 protoapp-twin 500 500 1000 false 02:00:00:00:00:bb + } > "$EV" + feed_and_settle || return + + local rep att shd + rep=$(field bytes reported); att=$(field bytes attributed); shd=$(field bytes shadowed) + note "reported=$rep attributed=$att shadowed=$shd" + + # SABOTAGE: in flow_identify(), replace the `dev_local` expression with a + # constant `true`. The external capture is then treated as its own device's + # traffic, shadowed falls to 0 and attributed rises to 2000. + chk "both captures are reported" "2000" "$rep" + chk "only one copy is attributed" "1000" "$att" + chk "the WAN-side twin is shadowed" "1000" "$shd" + + # SABOTAGE: any of the above. Conservation must hold across the shadow + # split as well, and this is the check that ties the three together: a + # daemon that shadowed a copy but forgot to report it would pass the three + # above individually while losing bytes. + chk "attributed + shadowed == reported" "2000" "$(( att + shd ))" + chk "leaked is still 0 across a shadow decision" "0" "$(field bytes leaked)" +} + +# -------------------------------------------------------------------- hostile + +test_hostile() { + head2 "HOSTILE INPUT: nothing off the wire is interpreted" + + # Everything in a flow record arrives from netifyd, which derives most of it + # from packets. host_server_name in particular is an SNI or an HTTP Host + # header: it is attacker-chosen, it reaches this daemon, and it is + # serialised into a ubus reply that a browser then renders. + # + # The daemon shells out to nothing, so the shell metacharacters below should + # be inert. This check is what makes that a tested property rather than an + # assumption, and it is cheap to keep. + local canary=/tmp/appflow-hostile-canary + rm -f "$canary" + { + hello + printf '{"type":"flow","internal":true,"interface":"br-lan","flow":{"digest":"O0000015","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","host_server_name":"$(touch %s)`touch %s`;touch %s","detected_application":1015,"detected_application_name":"protoapp-hostile","local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' "$canary" "$canary" "$canary" + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"O0000015","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":11,"other_bytes":22,"total_bytes":33}}\n' + } > "$EV" + feed_and_settle || return + + # THIS RUNTIME CHECK BINDS NOTHING TODAY, AND THE STATIC ONE BELOW IS THE + # REAL GUARD. A review panel converged on this and it was right. + # + # The canary cannot be turned red by any edit that leaves appflowd doing + # what it does, because appflowd never invokes a shell. The only edits that + # make it fire ADD a capability the daemon has never had. That is defect + # injection, not sabotage, so under the rule this file is written to it + # binds nothing and it must not be counted as behavioural coverage. + # + # It is kept, clearly labelled, because it costs one file existence test and + # it is the only thing that would catch a future maintainer adding a shell + # call on this path. What it is NOT is evidence that shell safety is tested. + if [ -e "$canary" ]; then + bad "a host_server_name was interpreted by a shell: $canary exists" + else + ok "(future-change guard, binds nothing today) no shell ran on the hostile name" + fi + + # THIS is the assertion that binds, and it binds because the property being + # asserted is itself a property of the text: appflowd's shell safety comes + # from the daemon containing no shell invocation at all, so "the source + # contains no such construct" is the guarantee rather than a stand-in for + # it. The defect and the text are the same object. + # + # SABOTAGE: add `system("true");` anywhere in root/usr/sbin/appflowd. This + # goes red and the canary above does not, which is the whole reason both + # are here. + local D=/usr/sbin/appflowd + if [ -r "$D" ]; then + local shellcalls + shellcalls=$(grep -cE '\b(system|popen|exec|spawn)[[:space:]]*\(' "$D") + chk "the daemon contains no shell or process-spawning construct" "0" "$shellcalls" + + # The same property one level up: a module it never imports is a + # capability it cannot reach by accident. + # + # SABOTAGE: add `import * as fs2 from "fs";`-style access to a process + # module. Pinned as an exact set because every import is a capability. + local imports + imports=$(grep -oE '^import .* from "[a-z]+"' "$D" | grep -oE '"[a-z]+"$' | tr -d '"' | sort | tr '\n' ' ') + chk "the daemon imports exactly the five modules it needs" \ + "fs socket ubus uci uloop " "$imports" + else + bad "cannot read $D to check it for shell constructs" + fi + + # SABOTAGE: in flow_identify(), remove the `substr(f.host_server_name, 0, 63)` + # bound. The stored host grows to whatever the peer sent. + chk "the flow was still accounted normally (11+22)" "33" "$(field bytes reported)" + + head2 "HOSTILE INPUT: reserved-looking key names are ordinary data" + + # In a JS engine these names reach Object.prototype and a lookup on a plain + # object literal returns a function rather than undefined. That defect was + # real in this package's BROWSER code and was fixed there with + # hasOwnProperty. + # + # ucode is not JavaScript and its objects carry no prototype chain, so the + # daemon has never been exposed to it. Verified directly: + # ucode -e 'let o = {}; print(o.constructor);' prints nothing + # + # The check stays because the property under test is "a device or + # application named like a language builtin is stored and returned as + # ordinary data", which is worth pinning on either engine, and because the + # sibling defect proves the input is reachable. It is honestly a weak check + # on this engine and section 7 of any review should say so. + { + hello + flow_ev P0000016 1016 constructor 10 10 20 true 02:00:00:00:00:aa + flow_ev Q0000017 1017 __proto__ 10 10 20 true 02:00:00:00:00:aa + flow_ev R0000018 1018 hasOwnProperty 10 10 20 true 02:00:00:00:00:aa + flow_ev S0000019 1019 toString 10 10 20 true 02:00:00:00:00:aa + } > "$EV" + feed_and_settle || return + + local apps rep + apps=$(field aggregates apps); rep=$(field bytes reported) + note "apps=$apps reported=$rep" + + # SABOTAGE: in agg_make(), change the `let a = tbl[key]` lookup to consult a + # fresh object literal seeded from tbl. On an engine with a prototype chain + # the four names below would then resolve to inherited functions; on ucode + # they would not, which is the honest limit of this check. + chk "four builtin-named applications produce four aggregates" "4" "$apps" + chk "and their bytes are accounted normally (4 x 20)" "80" "$rep" + + head2 "HOSTILE INPUT: the ubus reply survives control characters" + + # A name containing an embedded quote, a backslash and a newline escape. If + # any of them reached the reply unescaped, ubus's own JSON output would be + # malformed and the browser would fail to parse the whole document rather + # than showing one odd row. + { + hello + printf '{"type":"flow","internal":true,"interface":"br-lan","flow":{"digest":"T0000020","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","detected_application":1020,"detected_application_name":"ev\\"il\\\\name\\nsecond","local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"T0000020","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":5,"other_bytes":5,"total_bytes":10}}\n' + } > "$EV" + feed_and_settle || return + + # SABOTAGE: in the ubus reply path, build the JSON by string concatenation + # instead of handing the object to the ubus binding. The reply stops being + # parseable and this goes red. + # + # Parsed rather than pattern-matched, deliberately: a grep for a quote would + # assert on the spelling of the escape, while this asserts that the document + # is well formed, which is the property that matters to the browser. + if ubus call appflow summary 2>/dev/null | jsonfilter -e '@' >/dev/null 2>&1; then + ok "the summary reply is still well-formed JSON" + else + bad "a hostile application name made the ubus reply unparseable" + fi + chk "and the flow was accounted (5+5)" "10" "$(field bytes reported)" +} + +# ------------------------------------------------------------------ lifecycle + +test_lifecycle() { + head2 "LIFECYCLE: reset clears the counters it claims to" + + { + hello + flow_ev U0000021 1021 protoapp-reset 1234 4321 5555 true 02:00:00:00:00:aa + } > "$EV" + feed_and_settle || return + + local before tot_before tot_after rep att shd lk + before=$(field bytes reported) + tot_before=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + chk "fixture: bytes were counted before the reset" "5555" "$before" + chk "fixture: and the summary shows them" "5555" "$tot_before" + + ubus call appflow reset >/dev/null 2>&1 + sleep 2 + + tot_after=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + rep=$(field bytes reported); att=$(field bytes attributed) + shd=$(field bytes shadowed); lk=$(field bytes leaked) + note "after reset: summary=$tot_after reported=$rep attributed=$att shadowed=$shd leaked=$lk" + + # WHAT RESET ACTUALLY PROMISES, because the first version of this check got + # it wrong and read as a defect. + # + # It asserted `bytes.reported == 0` after a reset and failed with 5555. + # m_reset() zeroes the AGGREGATES -- totals, apps, devices, categories, and + # each flow's attributed up/down -- and deliberately does not touch + # `stats.bytes_*`, which are lifetime process counters. Its own comment says + # so. The summary is what the button clears and what the user sees; the + # conservation counters measure the daemon against netifyd for the life of + # the process and resetting them would destroy the only record of a leak. + # + # SABOTAGE: in m_reset(), delete the `agg_reset(totals, now)` call. The + # summary keeps its 5555 and this goes red. + chk "the summary total is zero after a reset" "0" "$tot_after" + + # The invariant is the part worth guarding, not any single counter's value. + # + # SABOTAGE: in m_reset(), add `stats.bytes_reported = 0;` and nothing else. + # reported drops to 0 while attributed stays 5555, so leaked computes + # negative and this goes red. That is the realistic way a future edit + # breaks this: somebody makes reset "more thorough" one counter at a time. + chk "the conservation invariant survives a reset (leaked still 0)" "0" "$lk" + chk "and reported still equals attributed + shadowed" "$rep" "$(( att + shd ))" + + note "note: bytes.* are lifetime counters and survive a reset by design" + + head2 "LIFECYCLE: the daemon reconnects when the peer goes away" + + local rc0 rc1 + rc0=$(field socket reconnects); rc0=${rc0:-0} + fake_down + sleep 3 + fake_up || { bad "could not bring the fake agent back up"; return; } + + local i=0 + while [ "$i" -lt 30 ]; do + [ "$(st | sed -n 's/.*"connected": *\(true\|false\).*/\1/p' | head -1)" = "true" ] && break + i=$((i + 1)); sleep 1 + done + rc1=$(field socket reconnects); rc1=${rc1:-0} + note "reconnects before=$rc0 after=$rc1" + + # SABOTAGE: in disconnect(), delete the schedule_reconnect() call. The + # daemon never comes back and both checks go red. Losing netifyd silently + # and permanently is the failure this guards: status would keep answering, + # with numbers frozen at the moment the agent went away. + chk "the daemon is connected again after the peer returned" "true" \ + "$(st | sed -n 's/.*"connected": *\(true\|false\).*/\1/p' | head -1)" + chk_ge "the reconnect was counted" "$((rc0 + 1))" "$rc1" +} + +# ------------------------------------------------------------------------ acl + +test_acl() { + head2 "ACL: the grant list matches what the package uses" + + local A=/usr/share/rpcd/acl.d/luci-app-appflow.json + [ -f "$A" ] || { bad "ACL file not installed"; return; } + + # These four are the same checks the hardware suite makes and they are + # repeated here on purpose: this suite is the one that can run without + # traffic, and an ACL check that only runs on the slow path is an ACL check + # that stops being run. + # + # SABOTAGE for each: add the named grant back to the ACL file. That file is + # part of the product, it is what rpcd enforces, and these are properties of + # its text rather than of a behaviour standing behind the text -- which is + # the case where reading source is a binding assertion rather than a hollow + # one. + if grep -q '"network"' "$A"; then + bad "ACL grants uci read on 'network' (PPPoE/wireguard secrets)" + else + ok "ACL does not grant uci read on 'network'" + fi + + if grep -q '"uci"' "$A"; then + bad "ACL contains a uci grant; this package reads only its own ubus object" + else + ok "ACL contains no uci grant at all" + fi + + if grep -q '"file"' "$A"; then + bad "ACL contains a file grant; this package shells out to nothing" + else + ok "ACL contains no file/exec grant" + fi + + local W + W=$(sed -n '/"write"/,$p' "$A" | grep -oE '"[a-z_]+"' | grep -vE '"write"|"ubus"|"appflow"' | tr -d '"' | tr '\n' ' ') + chk "the only write grant is appflow.reset" "reset" "$(echo "$W" | tr -d ' ')" + + # Every granted read method must be one the frontend actually declares, and + # every method the frontend declares must answer. The second half is the + # addition: the hardware suite checks that the ACL grants nothing extra, and + # nothing checked that a granted method exists at all. + # + # SABOTAGE: add `"nonexistent_method"` to the read grant list in the ACL. + # The loop below calls it, ubus refuses, and this goes red. A grant naming a + # method that does not exist is dead text that outlives the thing it named. + local m missing="" + for m in $(sed -n '/"read"/,/"write"/p' "$A" | grep -oE '"[a-z_]+"' \ + | grep -vE '"read"|"write"|"ubus"|"appflow"' | tr -d '"' | sort -u); do + ubus -v list appflow 2>/dev/null | grep -q "\"$m\"" || missing="$missing $m" + done + if [ -z "$(echo "$missing" | tr -d ' ')" ]; then + ok "every granted read method exists on the ubus object" + else + bad "granted but not offered by the daemon:$missing" + fi +} + +# ------------------------------------------------------------- AI breakout + +# A flow carrying an SNI. Separate from flow_ev() rather than an extra +# parameter on it, because every existing caller of flow_ev states its byte +# totals by hand and adding an optional field in the middle is how those calls +# silently shift by one. +# +# ai_flow +ai_flow() { + printf '{"type":"flow","internal":true,"interface":"br-lan","flow":{"digest":"%s","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_port":40001,"other_port":443,"ip_protocol":6,"host_server_name":"%s","detected_application":%s,"detected_application_name":"%s","detected_protocol":91,"detected_protocol_name":"TLS","category":{"application":10,"protocol":20},"local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' \ + "$1" "$4" "$2" "$3" + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"%s","local_ip":"192.168.9.10","local_mac":"02:00:00:00:00:aa","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":%s,"other_bytes":%s,"total_bytes":%s}}\n' \ + "$1" "$5" "$6" "$7" +} + +# The application label the daemon settled on for a given app name, read from +# the summary. Returns MISSING when absent, for the same reason field() does: +# a helper that cannot find its subject must turn its check red rather than +# return something plausible. +# jsonfilter, not sed. The first version of app_bytes() ranged from the "name" +# line to the next closing brace and looked for bytes_total inside it, but +# bytes_total appears BEFORE name in the object, so the range never contained +# it and every call returned the empty string. A structural query cannot make +# that mistake; a line-range over JSON can, and did. +app_present() { + local k + k=$(ubus call appflow summary '{"limit":50}' 2>/dev/null | + jsonfilter -e "@.top_apps[@.name=\"$1\"].key" | head -1) + [ -n "$k" ] && echo YES || echo NO +} + +# Bytes attributed to one application name. MISSING on a miss, never 0: a +# helper that cannot find its subject must turn its check red rather than +# return a number that happens to look plausible. +app_bytes() { + local v + v=$(ubus call appflow summary '{"limit":50}' 2>/dev/null | + jsonfilter -e "@.top_apps[@.name=\"$1\"].bytes_total" | head -1) + printf '%s' "${v:-MISSING}" +} + +# The category the daemon PUTS ON THE WIRE for one application. MISSING on a +# miss, never empty, so a helper that cannot find its subject turns its check +# red rather than comparing two blanks and passing. +app_cat() { + local v + v=$(ubus call appflow apps '{"limit":50}' 2>/dev/null | + jsonfilter -e "@.apps[@.name=\"$1\"].category" | head -1) + printf '%s' "${v:-MISSING}" +} + +# Bytes on one device aggregate. MISSING on a miss, never 0, so a helper that +# cannot find its subject turns its check red instead of comparing two blanks. +dev_bytes() { + local v + v=$(ubus call appflow devices '{"limit":20}' 2>/dev/null | + jsonfilter -e "@.devices[@.key=\"$1\"].bytes_total" | head -1) + printf '%s' "${v:-MISSING}" +} + +# A WAN-side flow the router itself originated: local_* is the router's own +# address and MAC, which is what makes is_router() true for the local side. +router_flow() { + printf '{"type":"flow","internal":false,"interface":"wan","flow":{"digest":"%s","local_ip":"%s","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_port":40001,"other_port":443,"ip_protocol":6,"detected_application":0,"detected_application_name":"","detected_protocol":91,"detected_protocol_name":"TLS","category":{"application":10,"protocol":20},"local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' \ + "$1" "$RIP" "$RMAC" + printf '{"type":"flow_stats","internal":false,"interface":"wan","flow":{"digest":"%s","local_ip":"%s","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":%s,"other_bytes":%s,"total_bytes":%s}}\n' \ + "$1" "$RIP" "$RMAC" "$2" "$3" "$4" +} + +# A second stats event on an existing router flow, carrying a larger cumulative +# counter. This is the update that used to discard the flow. +router_stats() { + printf '{"type":"flow_stats","internal":false,"interface":"wan","flow":{"digest":"%s","local_ip":"%s","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":%s,"other_bytes":%s,"total_bytes":%s}}\n' \ + "$1" "$RIP" "$RMAC" "$2" "$3" "$4" +} + +test_router_origin() { + head2 "ROUTER-ORIGINATED TRAFFIC SURVIVES CONNTRACK FORGETTING THE FLOW" + + # THE DEFECT, found 2026-08-31 on BOTH routers. The "Router" pseudo-device + # had never counted a single byte in production: 107,476 flows where + # conntrack positively confirmed the router originated the traffic, and a + # device row reading zero. The dashboard then correctly hides a zero-byte + # device, so the feature was invisibly absent rather than visibly broken. + # + # The mechanism, measured with temporary counters in the daemon rather than + # reasoned about: flow_identify() asks conntrack, gets a positive "the + # router originated this", keeps the flow and attaches it. Then the FIRST + # flow_stats event runs reshadow(), which asks the same question again. By + # then the connection has often closed and its conntrack entry is gone, so + # the answer is "unknown", and the code treated unknown as grounds to + # discard. Absence of evidence overrode a positive determination already + # made. That is the same mistake statusStrip() carries a comment about. + # + # This test pins the ordering that matters: resolve once, then let conntrack + # forget, and the bytes must still land. + local RIP RMAC CT + CT=/tmp/appflow-ct-fixture + RIP=$(jsonfilter -i /var/run/netifyd/status.json -e '@.interfaces.wan.addr[0]' 2>/dev/null) + case "$RIP" in + *.*) : ;; + *) RIP=$(jsonfilter -i /var/run/netifyd/status.json -e '@.interfaces.wan.addr[1]' 2>/dev/null) ;; + esac + RMAC=$(jsonfilter -i /var/run/netifyd/status.json -e '@.interfaces.wan.mac' 2>/dev/null) + + if [ -z "$RIP" ] || [ -z "$RMAC" ]; then + skip "no WAN address/mac in netifyd status; cannot forge a router flow" + return + fi + note "router WAN identity: $RIP / $RMAC" + + # A conntrack line in the kernel's own format. The daemon keys on the REPLY + # tuple and decides "was the source translated" by comparing the ORIGINAL + # src against the REPLY dst. Equal means no NAT, i.e. the router itself + # opened this connection. + printf 'ipv4 2 tcp 6 431999 ESTABLISHED src=%s dst=93.184.216.34 sport=40001 dport=443 packets=4 bytes=400 src=93.184.216.34 dst=%s sport=443 dport=40001 packets=4 bytes=400 [ASSURED] mark=0 zone=0 use=2\n' \ + "$RIP" "$RIP" > "$CT" + + uci set appflow.main.conntrack_path="$CT" + uci commit appflow + + # An internal flow first, so saw_internal is true: without it the whole + # shadow path is skipped and this test would pass against a daemon that had + # never worked. Then the router's own flow, and a SECOND stats update on it, + # because the second update is what used to destroy the flow. + # + # local_bytes/other_bytes are per-update deltas; total_bytes is cumulative. + { + hello + flow_ev RO000001 1001 lan-client 400 600 1000 true 02:00:00:00:00:aa + router_flow RO000002 700 1300 2000 + router_stats RO000002 700 1300 4000 + } > "$EV" + feed_and_settle || { uci delete appflow.main.conntrack_path; uci commit appflow; return; } + + # The conntrack block is the instrument this test depends on. Print it: a + # run where entries is 0 or available is null is not a failing product, it + # is a test that never armed, and the two must not look alike in the log. + note "conntrack: $(ubus call appflow status 2>/dev/null | jsonfilter -e '@.conntrack' 2>/dev/null)" + + # THE SABOTAGE FOR THIS ONE IS BOTH HALVES OF THE FIX, NOT EITHER, and that + # was established by running it rather than by reasoning. Reverting only the + # ct_nat_twin() port lookup leaves this GREEN, because the fr.ct_local guard + # then rescues the flow; reverting only the guard leaves it green too, + # because the lookup succeeds. Revert both -- key off `f.local_port` again + # AND drop the `if (fr.ct_local) return;` -- and this reads 0, which is what + # every release up to and including 1.1.1 did. + # + # The check below is the one that isolates the root cause on its own. + chk "a router-originated flow reaches the router device" "4000" "$(dev_bytes router)" + + # Same run, different question: the counter must show the lookup SUCCEEDING + # on the update, not merely that some bytes arrived. unresolved > 0 here + # means the key is not matching even though the entry is present. + # + # SABOTAGE: revert ct_nat_twin() to keying off `f.local_port`/`f.other_port` + # instead of the ports stored on the flow record. A flow_stats event carries + # no ports, so the key gains two empty fields and matches nothing. This is + # the precise detector for the shipped defect, and it fires on that revert + # alone even while the check above stays green. + chk "and conntrack resolved every re-check" "0" \ + "$(ubus call appflow status 2>/dev/null | jsonfilter -e '@.conntrack.unresolved' 2>/dev/null)" + + # THE MIRROR, and it is the one that matters most. A fix that simply stopped + # shadowing would pass both checks above and reintroduce the double-count + # this whole mechanism exists to prevent: a WAN-side copy of a NAT-ed client + # measured at 163-199% of wire truth. With conntrack unable to confirm the + # router originated anything, the flow must still be discarded. + # + # SABOTAGE: make reshadow() return unconditionally for dev_key "router". + # The two checks above stay green and this goes red. + : > "$CT" + { + hello + flow_ev RO000011 1001 lan-client 400 600 1000 true 02:00:00:00:00:aa + router_flow RO000012 700 1300 2000 + router_stats RO000012 700 1300 4000 + } > "$EV" + feed_and_settle + + note "with an empty conntrack: router = $(dev_bytes router)" + chk "an unconfirmable router flow is still shadowed, not counted" \ + "MISSING" "$(dev_bytes router)" + + uci delete appflow.main.conntrack_path 2>/dev/null + uci commit appflow + rm -f "$CT" +} + +test_ai_breakout() { + head2 "AI BREAKOUT (hostname matching, and the things it must NOT match)" + + # WHY THIS FEATURE EXISTS, measured: /etc/netify.d/netify-apps.conf is dated + # 10 August 2023, holds 199 signatures, and contains no AI vendor at all, so + # this traffic arrives as generic HTTP/S with detected_application 0. The SNI + # is already present in the flow record. Everything below drives the REAL + # daemon through the fake netifyd socket, so the oracle is the daemon's own + # output rather than a reimplementation of its table. + + uci set appflow.main.ai_breakout=1 2>/dev/null + uci commit appflow + + # Byte totals stated by hand, chosen so no two sums coincide: + # + # anthropic A local 1100 other 2200 total 3300 + # claude.ai B local 100 other 200 total 300 + # openai C local 11 other 22 total 33 + # evil D local 7 other 13 total 20 + # + # Anthropic (Claude) must aggregate A+B = 3600 into ONE row. + # sum(total) over all four = 3653 + { + hello + ai_flow AI000001 0 "" "api.anthropic.com" 1100 2200 3300 + ai_flow AI000002 0 "" "claude.ai" 100 200 300 + ai_flow AI000003 0 "" "api.openai.com" 11 22 33 + # SUFFIX ANCHORING. This host is registrable by anyone and a substring + # test on "anthropic.com" matches it. It must not be labelled Anthropic. + # + # SABOTAGE: in ai_match(), replace the label-boundary suffix rebuild with + # an index()/substring test. This check goes red on its own. + ai_flow AI000004 0 "" "anthropic.com.attacker.example" 7 13 20 + } > "$EV" + feed_and_settle + + chk "an SNI netifyd could not classify is labelled" \ + "YES" "$(app_present 'Anthropic (Claude)')" + chk "and so is a second AI vendor" "YES" "$(app_present 'OpenAI')" + + # THE MERGE. anthropic.com and claude.ai are one vendor. Keying on the + # matched suffix instead of the label would produce two identically-named + # rows, which looks like a rendering bug and hides the real total. + # + # SABOTAGE: in flow_identify(), set `key = "ai:" + ai.via`. The bytes split + # across two rows and this drops to 3300. + chk "two domains of one vendor aggregate into a single row" \ + "3600" "$(app_bytes 'Anthropic (Claude)')" + + # THE LOOKALIKE MUST LAND SOMEWHERE ELSE, and that is asserted by finding it + # where it belongs rather than by failing to find it where it does not. + # + # Two earlier versions of this check were TAUTOLOGIES, caught by running the + # sabotage rather than by reading them. One asked whether an app named + # "Anthropic (Claude)x" was present, which nothing can ever make true. The + # other grepped the summary for the hostname, which the summary does not + # carry at all. Both stayed green with the suffix matcher replaced by a + # substring test, which is the exact defect they were written for. + # + # anthropic.com.attacker.example arrives as app_id 0 over detected_protocol + # 91, so unlabelled it aggregates as "TLS" carrying its own 20 bytes. Under + # a substring matcher those bytes move into Anthropic and this reads MISSING. + # + # SABOTAGE: in ai_match(), match with index() instead of rebuilding the last + # N labels. + chk "a lookalike domain keeps its own identity, not the vendor it imitates" \ + "20" "$(app_bytes TLS)" + + # The counter must move, and by exactly the number of flows relabelled. + # Without this, "the label appeared" could come from anywhere. + # + # SABOTAGE: delete `stats.ai_labeled++`. + chk "exactly three flows were relabelled" "3" "$(field flows ai_labeled)" + + # THE CATEGORY IS A SLUG, AND NOTHING ELSE IN EITHER SUITE CHECKED IT. + # + # 1.1.0 shipped this column as four English display strings, "AI assistants" + # and three siblings, where every netifyd category is a lowercase slug. They + # read correctly in English and could therefore never be translated: the + # browser renders a category by looking the slug up in a table of _() + # literals, and a string that arrives already rendered has no key to look + # up. Every check in this group passed throughout, because all of them + # assert on labels, bytes and counters and none of them had ever looked at + # the category at all. + # + # This asserts the value ON THE WIRE, from the real daemon, which is the + # only place the contract is real. The frontend suite reads the same table + # out of the daemon source; that catches a bad edit, this catches a bad + # emission, and neither subsumes the other. + # + # SABOTAGE: in appflowd's AI_HOSTS table, change any category back to a + # display string, e.g. "ai-assistants" -> "AI assistants". This goes red and + # every other check in this group stays green. + chk "the AI category reaches the wire as a slug" \ + "ai-assistants" "$(app_cat 'Anthropic (Claude)')" + chk "and so does a second vendor's" "ai-assistants" "$(app_cat 'OpenAI')" + + # The mirror: a daemon that hardcoded one slug for everything would pass + # both rows above. A flow the AI table did NOT match must still carry the + # category netifyd gave it. + # + # ASSERTED AS A PROPERTY, NOT A LITERAL, AND THAT IS DELIBERATE. The first + # version expected "unclassified" and was simply wrong: the fixture sends + # category.application 10, and appflowd resolves that through + # catnames.app[] against the box's own /etc/netify.d/netify-categories.json, + # which on this bench is "file-sharing". Pasting that observed value back in + # would have made the check depend on one router's netifyd index, so a + # netifyd update would turn it red having broken nothing. What the check is + # actually for is that the AI block does not reach flows it never matched. + # + # SABOTAGE: set the category unconditionally in the AI block rather than + # from the table. This goes red while the two rows above stay green. + local tlscat verdict + tlscat=$(app_cat TLS) + case "$tlscat" in + MISSING|"") verdict=MISSING ;; + ai-*) verdict=AI-LEAKED ;; + *) verdict=OK ;; + esac + note "the unmatched TLS flow carries category '$tlscat'" + chk "a flow the AI table did not match keeps its netifyd category" \ + "OK" "$verdict" + + # ACCOUNTING IS UNTOUCHED. This is the check that matters most: the feature + # changes an identity and must not be able to change a byte. + # + # SABOTAGE: move the AI block after flow_attach(), or have it touch fr.a. + local tot + tot=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + chk "byte conservation holds with AI flows present (3300+300+33+20)" \ + "3653" "${tot:-MISSING}" + + head2 "AI BREAKOUT: netifyd's own answer wins unless we say otherwise" + + # THE SELF-RETIRING PROPERTY. If netifyd identified an application, its + # answer stands. That is what makes this table fall silent by itself the day + # Netify ship AI signatures, instead of permanently shadowing better data. + # + # SABOTAGE: drop the `app_id == 0` half of the gate in flow_identify(). The + # first check goes red. + { + hello + ai_flow AI000010 4242 "netify.some-cdn" "api.anthropic.com" 500 500 1000 + } > "$EV" + feed_and_settle + + # "netify.some-cdn" renders as "Some CDN" via display_name()/title_case(), + # read back from a live run rather than predicted. + chk "netifyd's application identification is NOT overridden" \ + "YES" "$(app_present 'Some CDN')" + chk "and the AI label was not applied over it" \ + "NO" "$(app_present 'Anthropic (Claude)')" + chk "so no flow was relabelled" "0" "$(field flows ai_labeled)" + + # THE EXCEPTION. A `strong` entry overrides even a netifyd identification, + # because netifyd recognises the PARENT brand and would swallow the service + # inside it: Gemini reported as Google, Copilot as GitHub. + # + # SABOTAGE: change gemini.google.com's third field to false. This goes red + # and the check above stays green, which is the distinction it exists for. + { + hello + ai_flow AI000011 4243 "netify.google" "gemini.google.com" 400 600 1000 + } > "$EV" + feed_and_settle + + chk "a strong entry DOES override netifyd's parent-brand answer" \ + "YES" "$(app_present 'Google Gemini')" + + head2 "AI BREAKOUT: hostile and awkward hostnames" + + # "constructor" and "toString.prototype" as hostnames. + # + # These are here to PROVE ucode behaves as the daemon's comment claims, not + # because they catch a live bug. In a browser `{}["constructor"]` returns a + # function and such a hostname would read as a match; in ucode there is no + # prototype chain and the lookup is null. Confirmed by sabotage: replacing + # the array type-check with a truthiness test changes no result here, so + # this group is NOT the reason that check exists and must not claim to be. + # + # What these hostnames do still prove is that a label-shaped input which is + # not a domain neither matches nor crashes the daemon. + # + # Also covers case folding and a trailing dot, both legal in a FQDN and both + # capable of shifting every suffix by one label. + { + hello + ai_flow AI000020 0 "" "constructor" 10 10 20 + ai_flow AI000021 0 "" "toString.prototype" 10 10 20 + ai_flow AI000022 0 "" "API.ANTHROPIC.COM" 30 30 60 + ai_flow AI000023 0 "" "api.openai.com." 40 40 80 + ai_flow AI000024 0 "" "notanthropic.com" 50 50 100 + ai_flow AI000025 0 "" "" 60 60 120 + } > "$EV" + feed_and_settle + + chk "an uppercase SNI still matches" "YES" "$(app_present 'Anthropic (Claude)')" + chk "a trailing-dot FQDN still matches" "YES" "$(app_present 'OpenAI')" + + # notanthropic.com shares a substring with anthropic.com but not a label + # boundary, and must not match. Counted rather than name-checked, because + # "is Anthropic present" is already true from AI000022 in this same batch. + # + # SABOTAGE: as for the lookalike above. + chk "exactly two of six awkward hostnames were relabelled" \ + "2" "$(field flows ai_labeled)" + + if [ "$(field bytes reported)" = "MISSING" ]; then + bad "status did not report byte totals after the hostile batch" + else + ok "the daemon survived the hostile hostname batch and still reports" + fi + + head2 "AI BREAKOUT: the upgrade path, where the option is absent" + + # THE UPGRADE PATH. apk treats /etc/config/appflow as a configuration file + # and leaves an existing one alone, so a config written by a version before + # this feature existed has no ai_breakout line at all. That is what every + # upgrading router has, which makes it the COMMON case, and it was the one + # case this group did not cover: the checks above set the option explicitly + # to 1, and the off-switch check sets it explicitly to 0. + # + # CFG_DEFAULTS says 1 and the daemon comment says "Absent means ON". This is + # the check that makes that true rather than merely claimed. + # + # SABOTAGE: change CFG_DEFAULTS.ai_breakout to 0, or make load_config() treat + # a missing value as off. Both go red here and nowhere else, because every + # other check in this file states the option explicitly. + uci delete appflow.main.ai_breakout 2>/dev/null + uci commit appflow + chk "the option really is absent (fixture guard)" "" "$(uci -q get appflow.main.ai_breakout)" + + { + hello + ai_flow AI000040 0 "" "api.anthropic.com" 300 300 600 + } > "$EV" + feed_and_settle + + chk "with no ai_breakout option at all, the feature is ON" \ + "1" "$(field flows ai_labeled)" + chk "and the vendor is labelled" "YES" "$(app_present 'Anthropic (Claude)')" + chk "status reports the default as enabled" "1" "$(field config ai_breakout)" + + head2 "AI BREAKOUT: the off switch" + + # A user who does not want an asserted classification must be able to turn + # it off completely, and the setting must be visible in status so support + # questions can be answered without asking for the config file. + # + # SABOTAGE: ignore cfg.ai_breakout in flow_identify(). + uci set appflow.main.ai_breakout=0 + uci commit appflow + { + hello + ai_flow AI000030 0 "" "api.anthropic.com" 100 100 200 + } > "$EV" + feed_and_settle + + chk "with ai_breakout off, nothing is relabelled" "0" "$(field flows ai_labeled)" + chk "and the AI label does not appear" "NO" "$(app_present 'Anthropic (Claude)')" + chk "status reports the setting" "0" "$(field config ai_breakout)" + + uci set appflow.main.ai_breakout=1 + uci commit appflow +} + +test_hostmap() { + head2 "HOSTMAP: teaching appflow a service it does not know" + + # WHY THIS EXISTS. Someone asked the obvious question of any traffic + # classifier: "I have a service I want measured, what do I do?" The answer + # used to be "edit the daemon and rebuild the package", which for a tool + # whose pitch is that netifyd's signatures are three years stale is the + # wrong shape entirely. + # + # `config hostmap` sections add to the built-in table, or override it. + + cp /etc/config/appflow /tmp/appflow.hostmap.keep + + # The worked example from the documentation, verbatim. If the docs and the + # code ever diverge, this is where it shows. + cat >> /etc/config/appflow <<'HMEOF' + +config hostmap + option suffix 'torproject.org' + option name 'Tor' + option category 'Privacy' +HMEOF + uci commit appflow 2>/dev/null + + { + hello + ai_flow HM000001 0 "" "check.torproject.org" 100 200 300 + ai_flow HM000002 0 "" "www.torproject.org" 10 20 30 + # NOT Tor: shares a substring but not a label boundary. Anyone can + # register this, and a substring matcher would label it Tor. + # + # SABOTAGE: make the matcher use index() instead of rebuilding the + # last N labels. This host is relabelled and the byte check goes red. + ai_flow HM000003 0 "" "torproject.org.evil.test" 7 13 20 + } > "$EV" + feed_and_settle + + chk "a user-supplied suffix is recognised" "YES" "$(app_present 'Tor')" + + # Both real hosts aggregate into the one application: 300 + 30 = 330. + # + # SABOTAGE: key the app on the matched suffix rather than the label. The + # rows split and this drops to 300. + chk "both hosts under it aggregate into one application" "330" "$(app_bytes Tor)" + + # The lookalike must keep its own identity. It is app_id 0 over + # detected_protocol 91, so unlabelled it aggregates as TLS with its own 20 + # bytes. + chk "a lookalike domain is not captured by a user entry" "20" "$(app_bytes TLS)" + + # The counter must move by exactly the number relabelled, so "the label + # appeared" cannot come from somewhere else. + chk "exactly two flows were relabelled" "2" "$(field flows ai_labeled)" + + head2 "HOSTMAP: overriding a built-in entry" + + # The same mechanism corrects a built-in that is wrong for someone's + # network, which is the second half of what extensibility means. + # + # SABOTAGE: consult AI_HOSTS before cfg.hostmap in the matcher. The + # built-in name comes back and this goes red. + cp /tmp/appflow.hostmap.keep /etc/config/appflow + cat >> /etc/config/appflow <<'HMEOF' + +config hostmap + option suffix 'anthropic.com' + option name 'Work AI' + option category 'Business' +HMEOF + uci commit appflow 2>/dev/null + + { + hello + ai_flow HM000010 0 "" "api.anthropic.com" 500 500 1000 + } > "$EV" + feed_and_settle + + chk "a user entry overrides the built-in name" "YES" "$(app_present 'Work AI')" + chk "and the built-in name is gone" "NO" "$(app_present 'Anthropic (Claude)')" + + head2 "HOSTMAP: malformed entries are refused, not half-applied" + + # A suffix with no dot can never match, because the matcher compares whole + # labels, so accepting one produces a rule that silently does nothing. + # + # THE GUARD'S ONLY OBSERVABLE EFFECT IS THE LOG LINE, and that is worth + # knowing. Removing the guard changes no classification at all: a dotless + # suffix matches nothing either way. What it changes is whether a user who + # fatfingered an entry is TOLD, or is left staring at a feature that appears + # not to work. An earlier version of this comment claimed the sabotage would + # redden a classification check; running it proved otherwise, so the check + # below asserts the thing that actually differs. + # + # SABOTAGE: drop the `index(suf, ".") < 0` guard. The warning disappears and + # the log check goes red. + cp /tmp/appflow.hostmap.keep /etc/config/appflow + # MARK THE LOG, do not count it. + # + # Two ways this went wrong before it worked. Grepping the whole history + # passes forever once a message has appeared even once, so the sabotage that + # removes the guard left the check green on a PREVIOUS run's warning. Then + # counting lines and reading past the count failed too, because logread is a + # RING: the buffer was already full at its maximum, the count stopped + # growing, and the window was always empty. A unique marker is immune to + # both. + local LOGTAG + LOGTAG="hostmap-test-$$-$(date +%s)" + logger -t appflow-suite "$LOGTAG" + cat >> /etc/config/appflow <<'HMEOF' + +config hostmap + option suffix 'notadomain' + option name 'Bad' + +config hostmap + option suffix 'example.net' + option name '' + +config hostmap + option suffix 'good.example' + option name 'Good' +HMEOF + uci commit appflow 2>/dev/null + + { + hello + ai_flow HM000020 0 "" "host.good.example" 40 60 100 + ai_flow HM000021 0 "" "notadomain" 10 10 20 + } > "$EV" + feed_and_settle + + chk "the valid entry still works alongside malformed ones" "YES" "$(app_present 'Good')" + chk "an entry with no name is refused" "NO" "$(app_present 'Bad')" + chk "only the valid flow was relabelled" "1" "$(field flows ai_labeled)" + + # The user must be TOLD which entry was skipped, by name. Silence here is + # indistinguishable from the feature being broken. + local NEWLOG + NEWLOG=$(logread 2>/dev/null | sed -n "/$LOGTAG/,\$p") + if printf '%s +' "$NEWLOG" | grep -q "hostmap: skipping entry with suffix notadomain"; then + ok "a dotless suffix is named in the log, not silently dropped" + else + bad "a dotless suffix was skipped without telling anyone" + fi + if printf '%s +' "$NEWLOG" | grep -q "hostmap: skipping entry with suffix example.net"; then + ok "an entry with no name is named in the log too" + else + bad "the nameless entry was skipped without telling anyone" + fi + + # Accounting is untouched by any of this: 100 + 20 = 120. + # + # SABOTAGE: have the hostmap path touch fr.a or run after flow_attach(). + local tot + tot=$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1) + chk "byte conservation holds with user entries present" "120" "${tot:-MISSING}" + + cp /tmp/appflow.hostmap.keep /etc/config/appflow + uci commit appflow 2>/dev/null + rm -f /tmp/appflow.hostmap.keep +} + +test_drilldown() { + head2 "DRILL-DOWN: device <-> application, from the live flow table" + + # WHY THIS EXISTS. "Who is using the bandwidth" was answerable and "what is + # that thing doing" was not. Both answers are one pass over the flow table, + # which already carries dev_key, app_key and cat_key on every record, so + # neither needed a new aggregate. That was worth checking rather than + # assuming: the first answer given was that it required a per-device + # per-application table and was therefore a memory design question. It did + # not, and the check took two minutes. + # + # Both directions report what CURRENTLY TRACKED flows account for, not + # lifetime totals, because a flow that has ended is no longer in the table. + # Every reply carries from_flows so a view cannot present one as the other. + + # Two devices, three applications, byte totals chosen so no two sums + # coincide and every assertion below is a distinct number. + # + # dev A (02:00:00:00:00:aa) app 4001 local 1000 other 2000 = 3000 + # dev A app 4002 local 100 other 200 = 300 + # dev B (02:00:00:00:00:bb) app 4001 local 10 other 20 = 30 + # + # app 4001 spans BOTH devices -> 3030 + # dev A spans BOTH applications -> 3300 + dev_flow() { + printf '{"type":"flow","internal":true,"interface":"br-lan","flow":{"digest":"%s","local_ip":"192.168.9.%s","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_port":40001,"other_port":443,"ip_protocol":6,"detected_application":%s,"detected_application_name":"%s","detected_protocol":91,"detected_protocol_name":"TLS","category":{"application":10,"protocol":20},"local_bytes":0,"other_bytes":0,"total_bytes":0}}\n' \ + "$1" "$2" "$3" "$4" "$5" + printf '{"type":"flow_stats","internal":true,"interface":"br-lan","flow":{"digest":"%s","local_ip":"192.168.9.%s","local_mac":"%s","other_ip":"93.184.216.34","other_mac":"02:00:00:00:00:ff","local_bytes":%s,"other_bytes":%s,"total_bytes":%s}}\n' \ + "$1" "$2" "$3" "$6" "$7" "$8" + } + + { + hello + dev_flow DD000001 10 02:00:00:00:00:aa 4001 "netify.alpha" 1000 2000 3000 + dev_flow DD000002 10 02:00:00:00:00:aa 4002 "netify.beta" 100 200 300 + dev_flow DD000003 11 02:00:00:00:00:bb 4001 "netify.alpha" 10 20 30 + # A flow whose LIFETIME counter exceeds what appflow attributed, which + # is what a stub adopted mid-transfer looks like: netifyd reports the + # connection's whole life, appflow only ever attributes what it saw. + # Without a row like this the two numbers are equal for every flow and + # summing the wrong one is invisible, which is exactly what happened: + # the sabotage below passed until this line existed. + dev_flow DD000004 12 02:00:00:00:00:cc 4003 "netify.gamma" 400 600 9000 + } > "$EV" + feed_and_settle + + # --- devices restricted to an application ----------------------------- + + # SABOTAGE: in m_devices, ignore req.args.apps and always take the + # aggregate path. from_flows goes false and every count below is wrong. + local A B + A=$(ubus call appflow devices '{"apps":["a4001"],"limit":10}' 2>/dev/null) + + chk "a device query restricted to one app says it came from flows" \ + "1" "$(printf '%s' "$A" | grep -c '"from_flows": true')" + chk "and finds BOTH devices carrying that application" \ + "2" "$(printf '%s' "$A" | jsonfilter -e '@.total' 2>/dev/null)" + + # The heavier device sorts first, and its bytes are its own, not the + # application's total across devices. + # + # SABOTAGE: sum fr.total instead of fr.up + fr.down. Device cc's flow has a + # lifetime counter of 9000 and 1000 attributed, so the sabotage reports + # 9000 and this goes red. Every other flow in the fixture has the two equal, + # which is why that row had to be added: without it the sabotage passed. + chk "the busier device is first" "3000" \ + "$(printf '%s' "$A" | jsonfilter -e '@.devices[0].bytes_total' 2>/dev/null)" + chk "and the quieter one carries only its own bytes" "30" \ + "$(printf '%s' "$A" | jsonfilter -e '@.devices[1].bytes_total' 2>/dev/null)" + + # An application nothing used yields an empty answer, not the whole list. + # + # SABOTAGE: treat an empty match set as "no filter". This returns every + # device, which is the failure that looks like the filter silently not + # working. + chk "an application with no flows yields no devices" "0" \ + "$(ubus call appflow devices '{"apps":["a9999"]}' 2>/dev/null | jsonfilter -e '@.total')" + + # Absent or empty `apps` must take the ordinary aggregate path, or every + # unfiltered page load starts answering from the flow table instead. + chk "no apps argument means the ordinary aggregate" "1" \ + "$(ubus call appflow devices '{"limit":5}' 2>/dev/null | grep -c '"from_flows": false')" + + # --- applications restricted to a device ------------------------------ + + # The mirror image, and the one a person actually reaches for. + # + # SABOTAGE: in apps_for_device, compare fr.app_key against dev instead of + # fr.dev_key. Everything below goes red at once. + B=$(ubus call appflow apps '{"device":"02:00:00:00:00:aa","limit":10}' 2>/dev/null) + + chk "an application query scoped to a device says it came from flows" \ + "1" "$(printf '%s' "$B" | grep -c '"from_flows": true')" + chk "and finds both applications that device used" \ + "2" "$(printf '%s' "$B" | jsonfilter -e '@.total' 2>/dev/null)" + chk "the heavier application is first" "3000" \ + "$(printf '%s' "$B" | jsonfilter -e '@.apps[0].bytes_total' 2>/dev/null)" + chk "and the lighter one is its own total" "300" \ + "$(printf '%s' "$B" | jsonfilter -e '@.apps[1].bytes_total' 2>/dev/null)" + + # The other device used only one application, which is the check that the + # scope is really per-device rather than global. + chk "a different device sees only its own applications" "1" \ + "$(ubus call appflow apps '{"device":"02:00:00:00:00:bb"}' 2>/dev/null | jsonfilter -e '@.total')" + + chk "a device with no flows yields nothing" "0" \ + "$(ubus call appflow apps '{"device":"02:00:00:00:00:zz"}' 2>/dev/null | jsonfilter -e '@.total')" + + chk "no device argument means the ordinary aggregate" "1" \ + "$(ubus call appflow apps '{"limit":5}' 2>/dev/null | grep -c '"from_flows": false')" + + # --- the argument policy --------------------------------------------- + + # ubus REJECTS an undeclared argument rather than ignoring it, and the + # rejection presents as the method returning nothing at all. Both new + # arguments had to be added to the published policy, and forgetting either + # is silent. + # + # SABOTAGE: remove `apps: []` or `device: ""` from the publish() policy. + chk "the devices policy declares apps" "1" \ + "$(ubus -v list appflow 2>/dev/null | grep -c '\"devices\":.*\"apps\":\"Array\"')" + chk "the apps policy declares device" "1" \ + "$(ubus -v list appflow 2>/dev/null | grep -c '\"apps\":.*\"device\":\"String\"')" + + # --- accounting is untouched ----------------------------------------- + + # Both directions read the flow table and write nothing. Conservation must + # hold exactly as it did: 3000 + 300 + 30. + # + # SABOTAGE: have either function mutate fr.up/fr.down while summing. + # ATTRIBUTED, NOT LIFETIME. Device cc contributes its attributed 1000, not + # its 9000 lifetime, so the total is 3000 + 300 + 30 + 1000. + # + # SABOTAGE: sum fr.total in either drill-down function. + chk "a drill-down reports ATTRIBUTED bytes, not the lifetime counter" "1000" \ + "$(ubus call appflow apps '{"device":"02:00:00:00:00:cc"}' 2>/dev/null | jsonfilter -e '@.apps[0].bytes_total')" + + chk "byte conservation is unaffected by either drill-down" "4330" \ + "$(ubus call appflow summary 2>/dev/null | sed -n 's/.*"bytes_total": *\([0-9]*\).*/\1/p' | head -1)" +} + +# ------------------------------------------------------------------------ run + +printf 'appflow protocol and bounds suite\n' +printf 'router: %s group: %s\n' \ + "$(uci -q get system.@system[0].hostname 2>/dev/null || echo '?')" "$GROUP" + +command -v socat >/dev/null 2>&1 || { printf 'socat is required and is not installed\n'; exit 2; } + +# Wait for the ubus object rather than demanding it instantly. +# +# appflowd registers a few seconds after procd starts it, so a bare check here +# fails whenever this suite is run right after something that restarted the +# daemon -- including the previous group of this same suite, whose cleanup +# restarts it. That happened on the first full pass: the hostile group exited 2 +# and reported the daemon absent while it was starting up two feet away. +# +# The guard itself is right and stays. An absent daemon must stop the run, not +# be tested around, because every counter would then read MISSING and the +# failures would blame the product. +i=0 +while [ "$i" -lt 20 ]; do + ubus list appflow >/dev/null 2>&1 && break + i=$((i + 1)); sleep 1 +done +ubus list appflow >/dev/null 2>&1 || { + printf 'the appflow ubus object never appeared; is appflowd installed and running?\n' + exit 2 +} + +ORIG_SOCK=$(uci -q get appflow.main.socket_path) +ORIG_FLOWMAX=$(uci -q get appflow.main.flow_max) +[ -n "$ORIG_SOCK" ] || { printf 'appflow.main.socket_path is unset; refusing to guess\n'; exit 2; } +printf 'saved socket_path=%s flow_max=%s\n' "$ORIG_SOCK" "$ORIG_FLOWMAX" + +# The test MACs must not be the router's own, or is_router() reclassifies every +# synthetic flow and the attribution checks assert on the wrong branch while +# still passing. Proving the fixture is valid costs one check and is the +# difference between a suite that tests attribution and one that tests nothing. +if ip link show 2>/dev/null | grep -qiE 'link/ether (02:00:00:00:00:aa|02:00:00:00:00:bb|02:00:00:00:00:ff)'; then + bad "fixture: a test MAC collides with a real interface on this router" +else + ok "fixture: no test MAC collides with a real interface" +fi + +uci set appflow.main.socket_path="$SOCK" +uci commit appflow + +case "$GROUP" in + conservation) test_conservation ;; + protocol) test_protocol ;; + bounds) test_bounds ;; + attribution) test_attribution ;; + hostile) test_hostile ;; + lifecycle) test_lifecycle ;; + acl) test_acl ;; + ai) test_ai_breakout ;; + router) test_router_origin ;; + hostmap) test_hostmap ;; + drilldown) test_drilldown ;; + all) test_conservation; test_protocol; test_bounds + test_attribution; test_hostile; test_lifecycle; test_acl + test_ai_breakout; test_hostmap; test_drilldown + test_router_origin ;; + *) printf "unknown group '%s'\n" "$GROUP"; exit 2 ;; +esac + +printf '\n----------------------------------------\n' +printf 'passed %d, failed %d\n' "$PASS" "$FAIL" diff --git a/luci-app-cf-dns-speedtest/.gitattributes b/luci-app-cf-dns-speedtest/.gitattributes new file mode 100644 index 000000000..88c7f9c60 --- /dev/null +++ b/luci-app-cf-dns-speedtest/.gitattributes @@ -0,0 +1,5 @@ +* text=auto eol=lf +root/usr/share/cf-dns-speedtest/cfst binary +*.png binary +*.ipk binary +*.zip binary diff --git a/luci-app-cf-dns-speedtest/.gitignore b/luci-app-cf-dns-speedtest/.gitignore new file mode 100644 index 000000000..569cc461e --- /dev/null +++ b/luci-app-cf-dns-speedtest/.gitignore @@ -0,0 +1,5 @@ +*.ipk +*.zip +SHA256SUMS +.DS_Store +Thumbs.db diff --git a/luci-app-cf-dns-speedtest/LICENSE b/luci-app-cf-dns-speedtest/LICENSE new file mode 100644 index 000000000..f288702d2 --- /dev/null +++ b/luci-app-cf-dns-speedtest/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/luci-app-cf-dns-speedtest/Makefile b/luci-app-cf-dns-speedtest/Makefile new file mode 100644 index 000000000..18c11c4ff --- /dev/null +++ b/luci-app-cf-dns-speedtest/Makefile @@ -0,0 +1,59 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-cf-dns-speedtest +PKG_VERSION:=1.1.0 +PKG_RELEASE:=1 +PKG_LICENSE:=GPL-3.0-only +PKG_MAINTAINER:=xiaokeikei + +include $(INCLUDE_DIR)/package.mk + +define Package/luci-app-cf-dns-speedtest + SECTION:=luci + CATEGORY:=LuCI + SUBMENU:=3. Applications + TITLE:=Cloudflare DNS speed test and scoped mosdns integration + DEPENDS:=+luci-base +mosdns + PKGARCH:=x86_64 +endef + +define Package/luci-app-cf-dns-speedtest/description + Selects a Cloudflare IPv4 address and serves it only for configured domains + through an isolated local mosdns instance. +endef + +define Package/luci-app-cf-dns-speedtest/conffiles +/etc/config/cf_dns_speedtest +endef + +define Build/Compile +endef + +define Package/luci-app-cf-dns-speedtest/install + $(CP) ./root/* $(1)/ +endef + +define Package/luci-app-cf-dns-speedtest/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +chmod 0755 /usr/bin/cf-dns-speedtest /usr/share/cf-dns-speedtest/cfst /etc/init.d/cf-dns-speedtest +/etc/init.d/cf-dns-speedtest enable +rm -rf /tmp/luci-indexcache /tmp/luci-modulecache +[ "$$(uci -q get cf_dns_speedtest.config.enabled)" = 1 ] && /usr/bin/cf-dns-speedtest apply >/dev/null 2>&1 +exit 0 +endef + +define Package/luci-app-cf-dns-speedtest/prerm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +/etc/init.d/cf-dns-speedtest stop 2>/dev/null +if [ -s /var/lib/cf-dns-speedtest/routes.active ]; then + while IFS= read -r route; do uci -q del_list dhcp.@dnsmasq[0].server="$${route}"; done < /var/lib/cf-dns-speedtest/routes.active +fi +uci -q commit dhcp +sed -i '/# cf-dns-speedtest$$/d' /etc/crontabs/root +/etc/init.d/dnsmasq restart 2>/dev/null +exit 0 +endef + +$(eval $(call BuildPackage,luci-app-cf-dns-speedtest)) diff --git a/luci-app-cf-dns-speedtest/README.md b/luci-app-cf-dns-speedtest/README.md new file mode 100644 index 000000000..e078ac798 --- /dev/null +++ b/luci-app-cf-dns-speedtest/README.md @@ -0,0 +1,51 @@ +# luci-app-cf-dns-speedtest — Cloudflare DNS IP 优选插件 + +适用于 OpenWrt / iStoreOS 的 Cloudflare DNS 优选插件。它调用 +[XIU2/CloudflareSpeedTest](https://github.com/XIU2/CloudflareSpeedTest) 测出较优的 +Cloudflare Anycast IPv4 地址,再通过独立 mosdns 旁路实例仅回答用户指定的域名。 + +## 设计边界 + +- 不替换 dnsmasq 的默认上游。 +- 不关闭或绕过 AdGuardHome、SmartDNS 等现有 DNS 服务。 +- 只有配置列表内的域名会转发到本插件的 mosdns 端口。 +- 测速失败时保留上一次成功的 IP 和 DNS 配置。 + +## v1.1.0 改进 + +- 修复定时任务未写入 crontab。 +- 精确记录并清理本插件拥有的 dnsmasq 分流规则,端口变更不再残留旧规则。 +- 日志和诊断输出设置硬上限,避免长期运行占用闪存。 +- 增加停止测速、上次测速时间和定时任务状态。 +- 增加参数范围、测速 URL 和 IPv4 结果校验。 +- 测速失败不覆盖上次结果,并尝试恢复上次可用 DNS 配置。 +- 修复 LuCI 中文乱码,优化窄屏状态卡和结果表格。 + +## 兼容性 + +- 已测试:iStoreOS 24.10.8,x86_64,Linux 6.6.144。 +- Release 中的 IPK 内含 x86_64 `cfst`,不可安装到 ARM/MIPS 设备。 +- 依赖:`luci-base`、`mosdns`。 + +## 安装 + +```sh +opkg install ./luci-app-cf-dns-speedtest_1.1.0-1_x86_64.ipk +``` + +升级会保留 `/etc/config/cf_dns_speedtest`。安装完成后进入 +“服务 → Cloudflare DNS 优选”,保存设置并点击“应用 DNS 配置”。 + +## 文件与服务 + +- `/etc/config/cf_dns_speedtest`:UCI 配置。 +- `/usr/bin/cf-dns-speedtest`:`start|run|stop|apply|status|ui`。 +- `/var/lib/cf-dns-speedtest/`:测速结果和生成的旁路 mosdns 配置。 +- `/etc/init.d/cf-dns-speedtest`:独立 mosdns 实例。 + +## 第三方组件与许可 + +本项目整体以 GPL-3.0-only 发布。内置的 `cfst` 来自 +[XIU2/CloudflareSpeedTest v2.3.5](https://github.com/XIU2/CloudflareSpeedTest/releases/tag/v2.3.5), +该项目同样采用 GPL-3.0。Cloudflare、OpenWrt、iStoreOS、mosdns 和 +CloudflareSpeedTest 均为其各自权利人的项目或商标,本项目与其不存在官方隶属关系。 diff --git a/luci-app-cf-dns-speedtest/THIRD_PARTY_NOTICES.md b/luci-app-cf-dns-speedtest/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..fd24dda20 --- /dev/null +++ b/luci-app-cf-dns-speedtest/THIRD_PARTY_NOTICES.md @@ -0,0 +1,10 @@ +# Third-party notices + +## XIU2/CloudflareSpeedTest + +- Component: `cfst` v2.3.5, linux/amd64 binary +- Upstream: https://github.com/XIU2/CloudflareSpeedTest +- License: GNU General Public License v3.0 only +- Source tag: https://github.com/XIU2/CloudflareSpeedTest/tree/v2.3.5 + +The binary is redistributed unmodified. Corresponding source is available from the upstream tag above. diff --git a/luci-app-cf-dns-speedtest/root/etc/config/cf_dns_speedtest b/luci-app-cf-dns-speedtest/root/etc/config/cf_dns_speedtest new file mode 100644 index 000000000..704b944ec --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/etc/config/cf_dns_speedtest @@ -0,0 +1,12 @@ +config main 'config' + option enabled '0' + option listen_port '5335' + option schedule '0 5 * * *' + option threads '4' + option probes '4' + option download_count '5' + option timeout '5' + option max_latency '300' + option min_speed '1' + option test_url 'https://speed.cloudflare.com/__down?bytes=20000000' + list domains 'www.cloudflare.com' diff --git a/luci-app-cf-dns-speedtest/root/etc/init.d/cf-dns-speedtest b/luci-app-cf-dns-speedtest/root/etc/init.d/cf-dns-speedtest new file mode 100755 index 000000000..74f883356 --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/etc/init.d/cf-dns-speedtest @@ -0,0 +1,28 @@ +#!/bin/sh /etc/rc.common + +START=91 +USE_PROCD=1 + +start_service() { + local enabled port + config_load cf_dns_speedtest + config_get_bool enabled config enabled 0 + [ "$enabled" = 1 ] || return 0 + [ -s /var/lib/cf-dns-speedtest/mosdns.yaml ] || /usr/bin/cf-dns-speedtest apply + procd_open_instance + procd_set_param command /usr/bin/mosdns start -c /var/lib/cf-dns-speedtest/mosdns.yaml + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param limits nofile="65535 65535" + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger cf_dns_speedtest +} + +reload_service() { + stop + start +} diff --git a/luci-app-cf-dns-speedtest/root/usr/bin/cf-dns-speedtest b/luci-app-cf-dns-speedtest/root/usr/bin/cf-dns-speedtest new file mode 100755 index 000000000..3a94a40e5 --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/usr/bin/cf-dns-speedtest @@ -0,0 +1,191 @@ +#!/bin/sh + +NAME=cf-dns-speedtest +STATE=/var/lib/$NAME +RUN=/var/run/$NAME +CFG=cf_dns_speedtest.config +LOG_MAX=131072 + +rotate_log() { + [ -f "$STATE/run.log" ] || return 0 + [ "$(wc -c < "$STATE/run.log")" -le "$LOG_MAX" ] && return 0 + tail -c 65536 "$STATE/run.log" > "$STATE/run.log.new" && mv "$STATE/run.log.new" "$STATE/run.log" +} + +log() { + mkdir -p "$STATE" + logger -t "$NAME" "$*" + printf '%s %s\n' "$(date '+%F %T')" "$*" >> "$STATE/run.log" + rotate_log +} + +cfg() { uci -q get "$CFG.$1"; } + +uint() { + case "$1" in ''|*[!0-9]*) printf '%s' "$2";; *) printf '%s' "$1";; esac +} + +domains() { + uci -q get "$CFG.domains" | tr ' ' '\n' | sed 's/^\.//;s/\.$//' | + grep -E '^[A-Za-z0-9]([A-Za-z0-9_-]*[A-Za-z0-9])?([.][A-Za-z0-9]([A-Za-z0-9_-]*[A-Za-z0-9])?)+$' | sort -u +} + +valid_ipv4() { + printf '%s\n' "$1" | awk -F. 'NF==4 { for (i=1;i<=4;i++) if ($i !~ /^[0-9]+$/ || $i>255) exit 1; exit 0 } { exit 1 }' +} + +remove_old_routes() { + local route old port + if [ -s "$STATE/routes.active" ]; then + while IFS= read -r route; do + [ -n "$route" ] && uci -q del_list dhcp.@dnsmasq[0].server="$route" + done < "$STATE/routes.active" + elif [ -s "$STATE/domains.active" ]; then + # Migration from versions before 1.1.0. Only remove domain-scoped + # routes owned by this package; never touch the generic DNS upstream. + port="$(uint "$(cfg listen_port)" 5335)" + while IFS= read -r old; do + [ -n "$old" ] && uci -q del_list dhcp.@dnsmasq[0].server="/$old/127.0.0.1#$port" + done < "$STATE/domains.active" + fi +} + +make_config() { + local ip="$1" port d route + port="$(uint "$(cfg listen_port)" 5335)" + [ "$port" -ge 1024 ] 2>/dev/null && [ "$port" -le 65535 ] 2>/dev/null || port=5335 + mkdir -p "$STATE" + domains > "$STATE/domains.new" + [ -s "$STATE/domains.new" ] || { rm -f "$STATE/domains.new"; log "no valid domains configured"; return 1; } + : > "$STATE/hosts.txt.new" + : > "$STATE/routes.new" + while IFS= read -r d; do + printf '%s %s\n' "$d" "$ip" >> "$STATE/hosts.txt.new" + printf '/%s/127.0.0.1#%s\n' "$d" "$port" >> "$STATE/routes.new" + done < "$STATE/domains.new" + cat > "$STATE/mosdns.yaml.new" </dev/null > "$tmp" || : + if [ -n "$schedule" ] && [ "$(cfg enabled)" = 1 ]; then + printf '%s /usr/bin/cf-dns-speedtest start # cf-dns-speedtest\n' "$schedule" >> "$tmp" + fi + mv "$tmp" /etc/crontabs/root + /etc/init.d/cron restart >/dev/null 2>&1 +} + +restart_dns() { + /etc/init.d/$NAME restart || return 1 + /etc/init.d/dnsmasq restart || return 1 +} + +apply() { + local ip + mkdir -p "$STATE" + ip="$(cat "$STATE/best_ip" 2>/dev/null)" + valid_ipv4 "$ip" || ip=1.1.1.1 + make_config "$ip" || return 1 + update_cron + restart_dns || { log "service restart failed"; return 1; } + log "configuration applied; active IP: $ip" +} + +run_test() { + local threads probes count timeout latency speed url ip old_ip + mkdir -p "$STATE" "$RUN" + if [ -f "$RUN/pid" ] && kill -0 "$(cat "$RUN/pid")" 2>/dev/null; then log "a test is already running"; return 2; fi + rm -f "$RUN/pid" + printf '%s\n' "$$" > "$RUN/pid" + trap 'rm -f "$RUN/pid" "$STATE/result.csv.new"' EXIT INT TERM + rotate_log + echo running > "$STATE/status" + threads="$(uint "$(cfg threads)" 4)"; [ "$threads" -ge 1 ] && [ "$threads" -le 64 ] || threads=4 + probes="$(uint "$(cfg probes)" 4)"; [ "$probes" -ge 1 ] && [ "$probes" -le 20 ] || probes=4 + count="$(uint "$(cfg download_count)" 5)"; [ "$count" -ge 1 ] && [ "$count" -le 20 ] || count=5 + timeout="$(uint "$(cfg timeout)" 5)"; [ "$timeout" -ge 1 ] && [ "$timeout" -le 30 ] || timeout=5 + latency="$(uint "$(cfg max_latency)" 300)"; [ "$latency" -ge 1 ] && [ "$latency" -le 9999 ] || latency=300 + speed="$(cfg min_speed)"; case "$speed" in ''|*[!0-9.]*) speed=1;; esac + url="$(cfg test_url)"; case "$url" in https://*|http://*) :;; *) echo failed > "$STATE/status"; log "invalid test URL"; return 1;; esac + cd /usr/share/$NAME || return 1 + log "speed test started" + if ./cfst -f ip.txt -o "$STATE/result.csv.new" -n "$threads" -t "$probes" -dn "$count" -dt "$timeout" -tl "$latency" -sl "$speed" -p 10 -url "$url" > "$STATE/cfst.out" 2>&1; then + ip="$(awk -F, 'NR==2 {gsub(/\r/,""); print $1; exit}' "$STATE/result.csv.new")" + if ! valid_ipv4 "$ip"; then echo failed > "$STATE/status"; log "no valid IPv4 result"; return 1; fi + old_ip="$(cat "$STATE/best_ip" 2>/dev/null)" + if ! make_config "$ip" || ! restart_dns; then + [ -n "$old_ip" ] && make_config "$old_ip" && restart_dns + echo failed > "$STATE/status"; log "new DNS configuration failed; previous IP restored"; return 1 + fi + mv "$STATE/result.csv.new" "$STATE/result.csv" + printf '%s\n' "$ip" > "$STATE/best_ip" + date '+%F %T' > "$STATE/last_run" + echo success > "$STATE/status" + log "speed test completed; best IP: $ip" + else + echo failed > "$STATE/status" + log "speed test failed; previous DNS configuration retained" + return 1 + fi + # Keep diagnostic output bounded without polluting the user-facing log. + if [ "$(wc -c < "$STATE/cfst.out")" -gt 65536 ]; then + tail -c 65536 "$STATE/cfst.out" > "$STATE/cfst.out.new" && mv "$STATE/cfst.out.new" "$STATE/cfst.out" + fi +} + +stop_test() { + if [ -f "$RUN/pid" ] && kill -0 "$(cat "$RUN/pid")" 2>/dev/null; then + kill "$(cat "$RUN/pid")" 2>/dev/null + log "speed test stopped" + fi + rm -f "$RUN/pid" + echo stopped > "$STATE/status" +} + +case "$1" in + start) mkdir -p "$STATE" "$RUN"; (run_test /dev/null 2>&1 &) ;; + run) run_test ;; + stop) stop_test ;; + apply) apply ;; + status) printf 'status=%s\nbest_ip=%s\nlast_run=%s\n' "$(cat "$STATE/status" 2>/dev/null || echo idle)" "$(cat "$STATE/best_ip" 2>/dev/null || echo '-')" "$(cat "$STATE/last_run" 2>/dev/null || echo '-')"; tail -n 20 "$STATE/run.log" 2>/dev/null ;; + ui) printf 'status=%s\nbest_ip=%s\nenabled=%s\nlast_run=%s\nscheduled=%s\n' "$(cat "$STATE/status" 2>/dev/null || echo idle)" "$(cat "$STATE/best_ip" 2>/dev/null || echo '-')" "$(cfg enabled)" "$(cat "$STATE/last_run" 2>/dev/null || echo '-')" "$(grep -c '# cf-dns-speedtest$' /etc/crontabs/root 2>/dev/null)"; [ -s "$STATE/result.csv" ] && cat "$STATE/result.csv" ;; + *) echo "Usage: $0 {start|run|stop|apply|status|ui}"; exit 1 ;; +esac diff --git a/luci-app-cf-dns-speedtest/root/usr/share/cf-dns-speedtest/cfst b/luci-app-cf-dns-speedtest/root/usr/share/cf-dns-speedtest/cfst new file mode 100644 index 000000000..8ccec6a9e Binary files /dev/null and b/luci-app-cf-dns-speedtest/root/usr/share/cf-dns-speedtest/cfst differ diff --git a/luci-app-cf-dns-speedtest/root/usr/share/cf-dns-speedtest/ip.txt b/luci-app-cf-dns-speedtest/root/usr/share/cf-dns-speedtest/ip.txt new file mode 100644 index 000000000..0fb313be0 --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/usr/share/cf-dns-speedtest/ip.txt @@ -0,0 +1,15 @@ +173.245.48.1 +103.21.244.1 +103.22.200.1 +103.31.4.1 +141.101.64.1 +108.162.192.1 +190.93.240.1 +188.114.96.1 +197.234.240.1 +198.41.128.1 +162.158.0.1 +104.16.0.1 +104.24.0.1 +172.64.0.1 +131.0.72.1 diff --git a/luci-app-cf-dns-speedtest/root/usr/share/luci/menu.d/luci-app-cf-dns-speedtest.json b/luci-app-cf-dns-speedtest/root/usr/share/luci/menu.d/luci-app-cf-dns-speedtest.json new file mode 100644 index 000000000..1172cc2c4 --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/usr/share/luci/menu.d/luci-app-cf-dns-speedtest.json @@ -0,0 +1,8 @@ +{ + "admin/services/cf-dns-speedtest": { + "title": "Cloudflare DNS 优选", + "order": 65, + "action": { "type": "view", "path": "cf-dns-speedtest" }, + "depends": { "acl": [ "luci-app-cf-dns-speedtest" ] } + } +} diff --git a/luci-app-cf-dns-speedtest/root/usr/share/rpcd/acl.d/luci-app-cf-dns-speedtest.json b/luci-app-cf-dns-speedtest/root/usr/share/rpcd/acl.d/luci-app-cf-dns-speedtest.json new file mode 100644 index 000000000..254fe13a9 --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/usr/share/rpcd/acl.d/luci-app-cf-dns-speedtest.json @@ -0,0 +1,19 @@ +{ + "luci-app-cf-dns-speedtest": { + "description": "Cloudflare DNS speedtest configuration", + "read": { + "uci": [ "cf_dns_speedtest" ], + "file": { + "/var/lib/cf-dns-speedtest/status": [ "read" ], + "/var/lib/cf-dns-speedtest/best_ip": [ "read" ], + "/var/lib/cf-dns-speedtest/run.log": [ "read" ] + } + }, + "write": { + "uci": [ "cf_dns_speedtest" ], + "file": { + "/usr/bin/cf-dns-speedtest": [ "exec" ] + } + } + } +} diff --git a/luci-app-cf-dns-speedtest/root/www/luci-static/resources/view/cf-dns-speedtest.js b/luci-app-cf-dns-speedtest/root/www/luci-static/resources/view/cf-dns-speedtest.js new file mode 100644 index 000000000..803790d07 --- /dev/null +++ b/luci-app-cf-dns-speedtest/root/www/luci-static/resources/view/cf-dns-speedtest.js @@ -0,0 +1,98 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require ui'; + +return view.extend({ + load: function() { + return L.resolveDefault(fs.exec('/usr/bin/cf-dns-speedtest', [ 'ui' ]), { + stdout: 'status=idle\nbest_ip=-\nenabled=0\nlast_run=-\nscheduled=0\n' + }); + }, + + render: function(data) { + var m, s, o; + m = new form.Map('cf_dns_speedtest', _('Cloudflare DNS 优选'), + _('测速后仅将指定域名交给独立 mosdns 实例返回优选 IP;其他 DNS 查询继续使用原有链路。')); + s = m.section(form.TypedSection, 'main', _('设置')); + s.anonymous = true; + o = s.option(form.Flag, 'enabled', _('启用')); + o.rmempty = false; + o = s.option(form.DynamicList, 'domains', _('Cloudflare 域名')); + o.datatype = 'hostname'; + o.placeholder = 'www.cloudflare.com'; + o = s.option(form.Value, 'listen_port', _('mosdns 旁路端口')); + o.datatype = 'port'; o.default = '5335'; + o = s.option(form.Value, 'schedule', _('定时任务')); + o.placeholder = '0 5 * * *'; + o.description = _('标准 cron 表达式,留空则不定时测速。保存后请点击“应用 DNS 配置”。'); + o = s.option(form.Value, 'threads', _('测速线程')); + o.datatype = 'range(1,64)'; o.default = '4'; + o = s.option(form.Value, 'probes', _('延迟测速次数')); + o.datatype = 'range(1,20)'; o.default = '4'; + o = s.option(form.Value, 'download_count', _('下载测速数量')); + o.datatype = 'range(1,20)'; o.default = '5'; + o = s.option(form.Value, 'timeout', _('单项超时(秒)')); + o.datatype = 'range(1,30)'; o.default = '5'; + o = s.option(form.Value, 'max_latency', _('最大延迟(毫秒)')); + o.datatype = 'range(1,9999)'; o.default = '300'; + o = s.option(form.Value, 'min_speed', _('最低速度(MB/s)')); + o.datatype = 'ufloat'; o.default = '1'; + o = s.option(form.Value, 'test_url', _('测速 URL')); + o.datatype = 'string'; + + s = m.section(form.NamedSection, 'runtime', 'runtime', _('运行状态')); + s.render = function() { + var lines = String(data.stdout || '').trim().split('\n'); + var status = (lines.shift() || 'status=idle').replace(/^status=/, ''); + var ip = (lines.shift() || 'best_ip=-').replace(/^best_ip=/, ''); + var enabled = (lines.shift() || 'enabled=0').replace(/^enabled=/, ''); + var lastRun = (lines.shift() || 'last_run=-').replace(/^last_run=/, ''); + var scheduled = (lines.shift() || 'scheduled=0').replace(/^scheduled=/, ''); + var rows = lines.filter(function(line) { return line.length; }).map(function(line) { return line.split(','); }); + var widths = [ '20%', '10%', '10%', '10%', '15%', '20%', '15%' ]; + var resultTable = rows.length ? E('div', { style: 'width:100%;overflow-x:auto;margin-top:1rem' }, [ + E('table', { style: 'display:table;width:100%;min-width:620px;table-layout:fixed;border-collapse:collapse' }, [ + E('thead', {}, E('tr', {}, rows[0].map(function(cell, index) { + return E('th', { style: 'width:' + widths[index] + ';padding:.65rem .6rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--border-color-medium,#444)' }, cell); + }))), + E('tbody', {}, rows.slice(1).map(function(row) { + return E('tr', {}, row.map(function(cell, index) { + return E('td', { style: 'width:' + widths[index] + ';padding:.65rem .6rem;text-align:left;white-space:nowrap;border-bottom:1px solid var(--border-color-low,#222)' }, cell); + })); + })) + ]) + ]) : E('p', { style: 'margin-top:1rem' }, _('尚无测速结果')); + function card(label, value) { + return E('div', { style: 'padding:.8rem;border:1px solid var(--border-color-medium,#333);border-radius:6px' }, [ + E('small', {}, label), E('div', { style: 'margin-top:.3rem;font-weight:600' }, value) + ]); + } + return E('div', { class: 'cbi-section' }, [ + E('div', { style: 'display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-bottom:1rem' }, [ + card(_('状态'), status), card(_('当前优选 IP'), ip), + card(_('DNS 应用'), enabled === '1' ? _('已启用') : _('未启用')), + card(_('上次测速'), lastRun), card(_('定时任务'), scheduled !== '0' ? _('已生效') : _('未生效')) + ]), + E('div', { style: 'display:flex;flex-wrap:wrap;gap:.5rem;align-items:center' }, [ + E('button', { class: 'btn cbi-button-action', click: ui.createHandlerFn(this, function() { + return fs.exec('/usr/bin/cf-dns-speedtest', [ 'start' ]).then(function() { + ui.addNotification(null, E('p', _('测速已在后台启动。'))); + window.setTimeout(function() { location.reload(); }, 1500); + }); + }) }, _('立即测速')), + E('button', { class: 'btn cbi-button-negative', click: ui.createHandlerFn(this, function() { + return fs.exec('/usr/bin/cf-dns-speedtest', [ 'stop' ]).then(function() { location.reload(); }); + }) }, _('停止测速')), + E('button', { class: 'btn cbi-button-apply', click: ui.createHandlerFn(this, function() { + return fs.exec('/usr/bin/cf-dns-speedtest', [ 'apply' ]).then(function() { location.reload(); }); + }) }, _('应用 DNS 配置')), + E('button', { class: 'btn', click: function() { location.reload(); } }, _('刷新状态')) + ]), + resultTable + ]); + }; + return m.render(); + } +}); diff --git a/luci-app-devgate/Makefile b/luci-app-devgate/Makefile new file mode 100644 index 000000000..105f90903 --- /dev/null +++ b/luci-app-devgate/Makefile @@ -0,0 +1,50 @@ +include $(TOPDIR)/rules.mk + +THEME_NAME:=devgate +PKG_NAME:=luci-app-devgate + +PKG_LICENSE:=Apache-2.0 + +LUCI_TITLE:=LuCI 设备门禁插件 +LUCI_DESCRIPTION:=基于 firewall4 通信规则的设备门禁插件。 +LUCI_DEPENDS:=+firewall4 +LUCI_PKGARCH:=all +LUCI_MINIFY_CSS:=0 + +PKG_VERSION:=3.2.4 +PKG_RELEASE:=1 +define Build/Compile +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + /etc/init.d/rpcd reload >/dev/null 2>&1 || true + /etc/init.d/devgate enable >/dev/null 2>&1 || true + /etc/init.d/devgate start >/dev/null 2>&1 || true + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache /tmp/luci-*cache +} +endef + +define Package/$(PKG_NAME)/prerm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + [ -x /etc/init.d/devgate ] && /etc/init.d/devgate stop >/dev/null 2>&1 || true + [ -x /usr/bin/devgate ] && /usr/bin/devgate stop >/dev/null 2>&1 || true + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache /tmp/luci-*cache +} +exit 0 +endef + +define Package/$(PKG_NAME)/postrm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + /etc/init.d/rpcd reload >/dev/null 2>&1 || true + rm -rf /tmp/luci-indexcache /tmp/luci-modulecache /tmp/luci-*cache +} +exit 0 +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-devgate/htdocs/luci-static/resources/view/devgate/custom.css b/luci-app-devgate/htdocs/luci-static/resources/view/devgate/custom.css new file mode 100644 index 000000000..1f9ab7727 --- /dev/null +++ b/luci-app-devgate/htdocs/luci-static/resources/view/devgate/custom.css @@ -0,0 +1,461 @@ +.devgate-page { + --devgate-control-border: #aeb7c2; + --devgate-control-border-strong: #8f9baa; + --devgate-selected-bg: #e8f1ff; + display: flex; + flex-direction: column; + min-height: 0; + height: calc(100vh - var(--devgate-page-offset, 0px)); +} + +.devgate-page .devgate-page-header > h2, +.devgate-page .devgate-rules-title { + margin: 0; + display: flex !important; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.devgate-page .devgate-page-header > h2 a { + color: inherit; + text-decoration: none; +} + +.devgate-page .cbi-map, +.devgate-page #cbi-devgate-device, +.devgate-page .devgate-rules-card-scroll { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + margin-bottom: 0; +} + +.devgate-page .devgate-rules-title .cbi-section-create { + margin: 0; + padding: 0; + display: flex; + justify-content: flex-end; +} + +.devgate-page .devgate-rules-card-scroll { + flex: 1 1 auto; + overflow: auto; +} + +.devgate-page .devgate-rule-cards { + display: flex; + flex-wrap: wrap; + align-content: flex-start; + gap: 0.75rem; + min-width: 0; + width: 100%; +} + +.devgate-page .devgate-rule-card { + box-sizing: border-box; + flex: 0 0 23rem; + width: 23rem; + max-width: 100%; + display: flex; + flex-direction: column; + gap: 0.65rem; + padding: 0.8rem; + border: 1px solid var(--devgate-control-border-strong); + border-radius: 8px; + background: var(--background-color-high, #fff); +} + +.devgate-page .devgate-rule-card > .devgate-card-field { + display: grid; + grid-template-columns: 4.5rem minmax(0, 1fr); + column-gap: 1rem; + align-items: center; + min-width: 0; + min-height: 2rem; + margin: 0; + padding: 0; + border: 0; +} + +.devgate-page .devgate-rule-card > .devgate-card-field > .cbi-value-title { + grid-column: 1; + float: none; + width: auto !important; + min-width: 0 !important; + margin: 0; + padding: 0; + color: var(--text-color-secondary, #666); + text-align: left; +} + +.devgate-page .devgate-rule-card > .devgate-card-field > .cbi-value-field { + grid-column: 2; + min-width: 0; + width: auto !important; + margin: 0; + padding: 0; +} + +.devgate-page .devgate-time-separator { + flex: 0 0 auto; + color: var(--text-color-secondary, #666); +} + +.devgate-page .devgate-rule-card > .devgate-card-uid { + display: none; +} + +.devgate-page .devgate-rule-card > .devgate-card-actions { + display: flex; + justify-content: flex-end; + min-width: 0; + width: 100%; +} + +.devgate-page .devgate-card-actions-content { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.75rem; + min-width: 0; +} + +.devgate-page .devgate-card-actions-content > .cbi-button-remove, +.devgate-page .devgate-rule-enable-button { + appearance: none; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + width: 4.75rem; + height: 2.125rem; + min-width: 4.75rem; + padding: 0 0.65rem; + border-radius: 4px; + font: inherit; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.2; + text-decoration: none; + text-transform: none; + vertical-align: middle; + white-space: nowrap; +} + +.devgate-page .devgate-card-actions > .devgate-card-actions-content > .devgate-card-enable { + display: flex; + align-items: center; + min-height: 0; + margin: 0; + padding: 0; + border: 0; +} + +.devgate-page .devgate-card-actions > .devgate-card-actions-content > .devgate-card-enable > .cbi-value-title { + display: none !important; +} + +.devgate-page .devgate-card-actions > .devgate-card-actions-content > .devgate-card-enable > .cbi-value-field { + min-width: 0; + width: auto !important; + margin: 0; + padding: 0; +} + +.devgate-page .devgate-rule-card input:not([type="checkbox"]), +.devgate-page .devgate-rule-card select, +.devgate-page .devgate-rule-card textarea { + min-width: 0 !important; + max-width: 100%; +} + +.devgate-page .devgate-rule-card .devgate-time-input { + min-width: 5em !important; + max-width: 5em !important; +} + +.devgate-page .devgate-label-proxy { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + border: 0; + opacity: 0; + pointer-events: none; +} + +.devgate-page .devgate-rule-card > .devgate-card-name input { + width: 100%; +} + +.devgate-page .devgate-rule-card > .devgate-card-name input, +.devgate-page .devgate-rule-card > .devgate-card-time-ranges .devgate-time-input { + border-color: var(--devgate-control-border) !important; +} + +.devgate-page .devgate-host-control { + position: relative; + min-width: 0; + width: 100%; +} + +.devgate-page .devgate-host-select { + display: block; + width: 100% !important; + min-height: 4.25rem; + padding: 0.45rem 0.65rem; + border: 1px solid var(--devgate-control-border) !important; + border-radius: 4px; + background: var(--background-color-high, #fff) !important; + text-align: left; + color: transparent !important; + box-shadow: none !important; + cursor: pointer; +} + +.devgate-page .devgate-host-select:hover, +.devgate-page .devgate-host-select:focus { + box-shadow: none !important; +} + +.devgate-page .devgate-host-display { + position: absolute; + inset: 0.45rem 0.65rem; + display: flex; + align-items: center; + min-width: 0; + overflow: hidden; + pointer-events: none; +} + +.devgate-page .devgate-host-choice { + display: grid; + gap: 0.12rem; + min-width: 0; + width: 100%; + max-width: 100%; + line-height: 1.2; +} + +.devgate-page .devgate-host-choice-name, +.devgate-page .devgate-host-choice-mac, +.devgate-page .devgate-host-choice-ip { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + line-height: 1.2; +} + +.devgate-page .devgate-host-choice-name { + color: var(--text-color-high, #222); + font-weight: 600; +} + +.devgate-page .devgate-host-choice-mac, +.devgate-page .devgate-host-choice-ip { + color: var(--text-color-secondary, #666); + font-size: 0.875rem; +} + +.devgate-host-picker { + position: fixed; + z-index: 1000; + display: none; + max-height: min(24rem, 60vh); + overflow: auto; + padding: 0.35rem; + border: 1px solid var(--devgate-control-border-strong); + border-radius: 8px; + background: var(--background-color-high, #fff); + box-shadow: 0 0.4rem 1.2rem rgba(0, 0, 0, 0.18); +} + +.devgate-host-picker.is-open { + display: grid; +} + +.devgate-host-picker-option { + display: block; + width: 100%; + padding: 0.45rem 0.55rem; + border: 0; + border-radius: 6px; + background: transparent; + text-align: left; + cursor: pointer; +} + +.devgate-host-picker-separator { + height: 1px; + margin: 0.25rem 0; + background: var(--devgate-control-border); + pointer-events: none; +} + +.devgate-host-picker-option:hover, +.devgate-host-picker-option:focus { + background: var(--background-color-medium, #f2f2f2); +} + +.devgate-host-picker-option.is-selected { + border-left: 3px solid var(--devgate-control-border-strong); + background: var(--devgate-selected-bg); +} + +.devgate-page .devgate-rule-card > .devgate-card-mode select, +.devgate-page .devgate-rule-card > .devgate-card-week select { + width: auto; +} + +.devgate-page .devgate-week-checkboxes { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.35rem 0.75rem; + min-width: 0; +} + +.devgate-page .devgate-week-checkbox { + display: inline-flex; + align-items: center; + gap: 0.25rem; + line-height: 1.25rem; + margin: 0; + white-space: nowrap; +} + +.devgate-page .devgate-week-checkbox input[type="checkbox"] { + position: static !important; + top: auto !important; + width: auto; + height: auto; + margin: 0; + vertical-align: middle; +} + +.devgate-page .devgate-week-checkbox span { + display: inline-flex; + align-items: center; + line-height: 1.25rem; +} + +.devgate-page .devgate-time-ranges { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + box-sizing: border-box; + min-width: 0; + height: 9em; + overflow-y: auto; + padding: 0.35rem; + border: 1px solid var(--devgate-control-border); + border-radius: 4px; +} + +.devgate-page .devgate-time-range-row { + display: grid; + grid-template-columns: 5em max-content 5em minmax(0, 1fr) 4em; + align-items: center; + column-gap: 0.45rem; + min-width: 0; + width: 100%; + max-width: 100%; +} + +.devgate-page .devgate-rule-card > .devgate-card-time-ranges .devgate-time-input { + box-sizing: border-box; + flex: 0 0 5em; + width: 5em !important; + min-width: 5em !important; + max-width: 5em !important; + height: 2rem; + text-align: center; +} + +.devgate-page .devgate-time-range-action { + grid-column: 5; + box-sizing: border-box; + width: 4em !important; + min-width: 4em !important; + max-width: 4em !important; + height: 2rem; + padding: 0 0.65rem; +} + +.devgate-page .devgate-time-range-add { + border-color: #1d6fb8 !important; + background: #247fc9 !important; + color: #fff !important; +} + +.devgate-page .devgate-time-range-remove { + border-color: #ba651c !important; + background: #d97823 !important; + color: #fff !important; +} + +.devgate-page .devgate-rule-card > .devgate-card-mode .cbi-radio { + display: inline-flex; + align-items: center; + gap: 0.2rem; + margin-right: 0.85rem; + white-space: nowrap; +} + +.devgate-page .devgate-rule-enable-control { + display: flex; + align-items: center; +} + +.devgate-page .devgate-rule-enable-input { + position: absolute !important; + width: 1px !important; + height: 1px !important; + margin: -1px !important; + padding: 0 !important; + border: 0 !important; + opacity: 0; + pointer-events: none; +} + +.devgate-page .devgate-rule-enable-button { + border: 1px solid transparent; + color: #fff; + cursor: pointer; +} + +.devgate-page .devgate-rule-enable-button::before { + content: "未启用"; +} + +.devgate-page .devgate-rule-enable-input:checked + .devgate-rule-enable-button { + border-color: #238846; + background: #2f9e55; +} + +.devgate-page .devgate-rule-enable-input:checked + .devgate-rule-enable-button::before { + content: "已启用"; +} + +.devgate-page .devgate-rule-enable-input:not(:checked) + .devgate-rule-enable-button { + border-color: #8f9baa; + background: #9aa3ad; +} + +.devgate-page .devgate-rule-enable-input:disabled + .devgate-rule-enable-button { + cursor: default; + opacity: 0.65; +} + +@media (max-width: 36rem) { + .devgate-page .devgate-rule-card { + flex-basis: 100%; + width: 100%; + } +} diff --git a/luci-app-devgate/htdocs/luci-static/resources/view/devgate/overview.js b/luci-app-devgate/htdocs/luci-static/resources/view/devgate/overview.js new file mode 100644 index 000000000..8353e0690 --- /dev/null +++ b/luci-app-devgate/htdocs/luci-static/resources/view/devgate/overview.js @@ -0,0 +1,1635 @@ +// SPDX-License-Identifier: Apache-2.0 +'use strict'; +'require view'; +'require ui'; +'require uci'; +'require form'; +'require dom'; +'require fs'; +'require request'; +'require rpc'; + +var DEV_GATE_ASSET_VERSION = Date.now(); +var callHostHints = rpc.declare({ + object: 'luci-rpc', + method: 'getHostHints', + expect: { '': {} } +}); +var callDevGateConfig = rpc.declare({ + object: 'uci', + method: 'get', + params: [ 'config' ], + expect: { values: {} }, + reject: true +}); +var HOST_PICKER_ID = 'devgate-host-picker-options'; +var DEV_GATE_STATUS_REFRESH_INTERVAL = 1000; +var DEV_GATE_MAX_TIME_RANGES = 24; +var DEV_GATE_DEFAULT_TIME_RANGE = '00:00-00:00'; +var activeHostPicker = null; +var hostPickerCloseInstalled = false; +var WEEK_DAYS = [ + { value: '7', label: _('周日') }, + { value: '1', label: _('周一') }, + { value: '2', label: _('周二') }, + { value: '3', label: _('周三') }, + { value: '4', label: _('周四') }, + { value: '5', label: _('周五') }, + { value: '6', label: _('周六') } +]; + +function normalizeRuleUid(value) { + var uid = String(value == null ? '' : value).toLowerCase().replace(/[^a-z0-9]/g, '').substring(0, 6); + + return /^[a-z0-9]{6}$/.test(uid) ? uid : null; +} + +function randomRuleUid() { + var value = Math.floor(Math.random() * 0x1000000); + var bytes; + + if (window.crypto && window.crypto.getRandomValues) { + bytes = new Uint8Array(3); + window.crypto.getRandomValues(bytes); + value = (bytes[0] << 16) | (bytes[1] << 8) | bytes[2]; + } + + return ('000000' + value.toString(16)).slice(-6); +} + +function createRuleUid(usedRuleUids) { + var uid; + + do { + uid = randomRuleUid(); + } while (usedRuleUids[uid]); + + return uid; +} + +function resolveRuleUid(sectionId, cfgvalue, usedRuleUids, sectionRuleUids) { + var uid; + + if (sectionRuleUids[sectionId]) { + return sectionRuleUids[sectionId]; + } + + uid = normalizeRuleUid(cfgvalue); + + if (!uid || (usedRuleUids[uid] && usedRuleUids[uid] !== sectionId)) { + uid = createRuleUid(usedRuleUids); + } + + usedRuleUids[uid] = sectionId; + sectionRuleUids[sectionId] = uid; + + return uid; +} + +function getPackageVersion() { + return L.resolveDefault(fs.read_direct('/usr/lib/opkg/status'), '') + .then(function (status) { + var block = String(status || '').match(/(^|\n)Package:\s*luci-app-devgate\n([\s\S]*?)(\n\n|$)/); + var version = block ? block[2].match(/(^|\n)Version:\s*([^\s]+)/) : null; + + return version ? version[2] : 'unknown'; + }) + .catch(function () { + return 'unknown'; + }); +} + +function renderPageHeader(version) { + return E('div', { 'class': 'devgate-page-header' }, [ + E('h2', {}, [ + E('a', { + href: 'https://github.com/Antecer/luci-app-devgate', + target: '_blank', + rel: 'noreferrer noopener' + }, 'DevGate v%s'.format(version || 'unknown')) + ]) + ]); +} + +function renderStylesheet(version) { + return E('link', { + rel: 'stylesheet', + href: L.resource('view/devgate/custom.css') + '?v=' + encodeURIComponent(version || 'dev') + '-' + DEV_GATE_ASSET_VERSION + }); +} + +function refreshRuleEnableStates(mapEl) { + return callDevGateConfig('devgate') + .then(function (sections) { + Object.keys(sections || {}).forEach(function (sectionId) { + var section = sections[sectionId]; + var sid; + + if (!section || section['.type'] !== 'device') { + return; + } + + sid = section['.name'] || sectionId; + var field = mapEl.querySelector('.cbi-value[data-field="cbid.devgate.%s.enable"]'.format(sid)); + var wrapper = field ? field.querySelector('.devgate-rule-enable-control') : null; + var input = wrapper ? wrapper.querySelector('.devgate-rule-enable-input') : null; + var enabled = String(section.enable || '0') !== '0'; + + if (!input || wrapper.getAttribute('data-changed') === 'true') { + return; + } + + if (input.checked !== enabled) { + input.checked = enabled; + } + }); + }) + .catch(function (err) { + console.warn('DevGate状态刷新失败:', err); + }); +} + +function watchRuleEnableStates(mapEl) { + var timer = window.setInterval(function () { + if (!document.body.contains(mapEl)) { + window.clearInterval(timer); + return; + } + + refreshRuleEnableStates(mapEl); + }, DEV_GATE_STATUS_REFRESH_INTERVAL); +} + +function closeHostPicker() { + if (!activeHostPicker) { + return; + } + + activeHostPicker.button.removeAttribute('aria-expanded'); + activeHostPicker.input.dispatchEvent(new Event('change', { bubbles: true })); + activeHostPicker = null; + + var picker = document.getElementById(HOST_PICKER_ID); + + if (picker) { + picker.classList.remove('is-open'); + picker.removeAttribute('data-target-input'); + } +} + +function renderHostChoice(choice) { + return E('span', { 'class': 'devgate-host-choice' }, [ + E('span', { 'class': 'devgate-host-choice-name' }, choice.name), + E('span', { 'class': 'devgate-host-choice-mac' }, choice.mac.toUpperCase()), + E('span', { 'class': 'devgate-host-choice-ip' }, choice.ipText) + ]); +} + +function renderHostButtonValue(choice) { + return choice ? '%s %s %s'.format(choice.name, choice.mac.toUpperCase(), choice.ipText) : _('选择目标设备'); +} + +function syncHostDisplay(root, choice) { + var display = root.querySelector('.devgate-host-display'); + var button = root.querySelector('.devgate-host-select'); + + if (button) { + button.value = renderHostButtonValue(choice); + } + + if (!display) { + return; + } + + dom.content(display, choice ? [renderHostChoice(choice)] : [_('选择目标设备')]); +} + +function selectHostChoice(choice) { + if (!activeHostPicker) { + return; + } + + activeHostPicker.input.value = choice.mac; + syncHostDisplay(activeHostPicker.root, choice); + closeHostPicker(); +} + +function renderHostPicker(hostChoices) { + if (!hostPickerCloseInstalled) { + document.addEventListener('click', function (ev) { + var picker = document.getElementById(HOST_PICKER_ID); + + if (!activeHostPicker) { + return; + } + + if ((picker && picker.contains(ev.target)) || activeHostPicker.root.contains(ev.target)) { + return; + } + + closeHostPicker(); + }); + hostPickerCloseInstalled = true; + } + + return E('div', { id: HOST_PICKER_ID, 'class': 'devgate-host-picker' }, hostChoices.reduce(function (nodes, host, index) { + nodes.push(E('button', { + type: 'button', + 'class': 'devgate-host-picker-option', + 'data-mac': host.mac, + click: function () { + selectHostChoice(host); + } + }, [renderHostChoice(host)])); + + if (index < hostChoices.length - 1) { + nodes.push(E('div', { + 'class': 'devgate-host-picker-separator', + role: 'separator' + })); + } + + return nodes; + }, [])); +} + +function openHostPicker(root, button, input, currentMac) { + var picker = document.getElementById(HOST_PICKER_ID); + var rect; + + if (!picker) { + return; + } + + if (activeHostPicker && activeHostPicker.root === root) { + closeHostPicker(); + return; + } + + closeHostPicker(); + activeHostPicker = { + root: root, + button: button, + input: input + }; + + rect = button.getBoundingClientRect(); + picker.classList.add('is-open'); + picker.setAttribute('data-target-input', input.id); + picker.style.left = '%spx'.format(Math.round(rect.left)); + picker.style.top = '%spx'.format(Math.round(rect.bottom + 4)); + picker.style.width = '%spx'.format(Math.round(rect.width)); + button.setAttribute('aria-expanded', 'true'); + + Array.prototype.forEach.call(picker.querySelectorAll('.devgate-host-picker-option'), function (option) { + option.classList.toggle('is-selected', option.getAttribute('data-mac') === currentMac); + }); +} + +function getRuleField(card, option) { + return card.querySelector('.cbi-value[data-name="%s"]'.format(option)); +} + +function findNodeById(root, id) { + var nodes = root ? root.querySelectorAll('[id]') : []; + + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].id === id) { + return nodes[i]; + } + } + + return null; +} + +function isLabelableElement(node) { + var tag; + var type; + + if (!node || !node.tagName) { + return false; + } + + tag = node.tagName.toLowerCase(); + + if (tag === 'input') { + type = String(node.type || '').toLowerCase(); + return type !== 'hidden'; + } + + return tag === 'button' || tag === 'meter' || tag === 'output' || tag === 'progress' || tag === 'select' || tag === 'textarea'; +} + +function ensureRuleFieldLabelTarget(field) { + var title = field ? field.querySelector('.cbi-value-title[for]') : null; + var valueField = field ? field.querySelector('.cbi-value-field') : null; + var id = title ? title.getAttribute('for') : null; + var labelTarget; + + if (!id || isLabelableElement(findNodeById(field, id))) { + return; + } + + if (field.classList.contains('devgate-card-uid')) { + title.removeAttribute('for'); + return; + } + + labelTarget = (valueField || field).querySelector('button, meter, output, progress, select, textarea, input:not([type="hidden"])'); + if (labelTarget && labelTarget.id && isLabelableElement(labelTarget)) { + title.setAttribute('for', labelTarget.id); + return; + } + + (valueField || field).insertBefore(E('input', { + id: id, + type: 'button', + 'class': 'devgate-label-proxy', + tabindex: '-1', + 'aria-hidden': 'true', + value: '' + }), (valueField || field).firstChild); +} + +function setupRuleCardField(card, option, className) { + var field = getRuleField(card, option); + + if (field) { + field.classList.add('devgate-card-field', className); + ensureRuleFieldLabelTarget(field); + } + + return field; +} + +function setupRuleCard(section, configName, sectionId, node) { + var removeTitle = section.titleFn('delbtntitle', sectionId); + var card = E('div', { + 'id': 'cbi-%s-%s'.format(configName, sectionId), + 'class': 'cbi-section-node devgate-rule-card', + 'data-section-id': sectionId + }, node); + var enable; + var actions; + + setupRuleCardField(card, 'comment', 'devgate-card-name'); + setupRuleCardField(card, 'uid', 'devgate-card-uid'); + setupRuleCardField(card, 'mac', 'devgate-card-target'); + setupRuleCardField(card, 'chain', 'devgate-card-mode'); + setupRuleCardField(card, 'time_ranges', 'devgate-card-time-ranges'); + setupRuleCardField(card, 'week', 'devgate-card-week'); + enable = setupRuleCardField(card, 'enable', 'devgate-card-enable'); + + actions = E('div', { 'class': 'devgate-card-actions' }, [ + E('div', { 'class': 'devgate-card-actions-content' }, [ + E('button', { + 'class': 'cbi-button cbi-button-remove', + 'name': 'cbi.rts.%s.%s'.format(configName, sectionId), + 'data-section-id': sectionId, + 'click': ui.createHandlerFn(section, 'handleRemove', sectionId), + 'disabled': section.map.readonly || null + }, [removeTitle || _('删除')]) + ]) + ]); + + if (enable) { + actions.querySelector('.devgate-card-actions-content').appendChild(enable); + } + + card.appendChild(actions); + + return card; +} + +function renderRulesContents(cfgsections, nodes) { + var configName = this.uciconfig || this.map.config; + var sectionEl = E('div', { + 'id': 'cbi-%s-%s'.format(configName, this.sectiontype), + 'class': 'cbi-section devgate-rules-section', + 'data-tab': (this.map.tabbed && !this.parentoption) ? this.sectiontype : null, + 'data-tab-title': (this.map.tabbed && !this.parentoption) ? this.title || this.sectiontype : null + }); + var addEl = this.renderSectionAdd('devgate-rules-add'); + var titleEl = E('h3', { 'class': 'devgate-rules-title' }, [ + this.title || _('设备规则'), + addEl + ]); + var gridEl = E('div', { 'class': 'devgate-rule-cards' }); + var i; + + sectionEl.appendChild(titleEl); + + if (this.description != null && this.description !== '') { + sectionEl.appendChild(E('div', { 'class': 'cbi-section-descr' }, this.description)); + } + + for (i = 0; i < nodes.length; i++) { + gridEl.appendChild(setupRuleCard(this, configName, cfgsections[i], nodes[i])); + } + + sectionEl.appendChild(E('div', { 'class': 'devgate-rules-card-scroll' }, gridEl)); + dom.bindClassInstance(sectionEl, this); + + return sectionEl; +} + +function normalizeIp(ip) { + if (ip == null) { + return null; + } + + ip = String(ip).trim(); + + if (ip === '') { + return null; + } + + if (ip.indexOf(',') !== -1) { + ip = ip.split(',')[0].trim(); + } + + if (ip.indexOf('::ffff:') === 0) { + ip = ip.substring(7); + } + + if (/^\[[0-9a-fA-F:]+\](?::\d+)?$/.test(ip)) { + return ip.replace(/^\[/, '').replace(/\](?::\d+)?$/, '').toLowerCase(); + } + + if (/^([0-9]{1,3}\.){3}[0-9]{1,3}:\d+$/.test(ip)) { + ip = ip.replace(/:\d+$/, ''); + } + + if (/^([0-9]{1,3}\.){3}[0-9]{1,3}\/[0-9]{1,2}$/.test(ip)) { + ip = ip.replace(/\/[0-9]{1,2}$/, ''); + } else if (/^[0-9a-fA-F:]+\/[0-9]{1,3}$/.test(ip)) { + ip = ip.replace(/\/[0-9]{1,3}$/, ''); + } + + return ip.toLowerCase(); +} + +function normalizeMac(mac) { + if (mac == null) { + return null; + } + + mac = String(mac).trim().toLowerCase(); + + return /^([0-9a-f]{2}:){5}[0-9a-f]{2}$/.test(mac) ? mac : null; +} + +function extractMac(value) { + var text = String(value == null ? '' : value); + var direct = normalizeMac(text); + var match; + + if (direct) { + return direct; + } + + match = text.match(/([0-9a-f]{2}:){5}[0-9a-f]{2}/i); + + return match ? normalizeMac(match[0]) : null; +} + +function getEnvRemoteAddress() { + var keys = ['remote_addr', 'REMOTE_ADDR', 'peeraddr', 'client_ip']; + + for (var i = 0; i < keys.length; i++) { + var value = normalizeIp(L.env && L.env[keys[i]]); + + if (value) { + return value; + } + } + + return null; +} + +function addClientIp(ips, value) { + var ip = normalizeIp(value); + + if (ip) { + ips[ip] = true; + } +} + +function parseClientIps(text) { + var ips = {}; + + String(text || '').split(/[\r\n]+/).forEach(function (line) { + line = line.trim(); + + if (!line) { + return; + } + + line.split(',').forEach(function (part) { + var forwarded = String(part || '').match(/for="?([^;,"]+)/i); + + addClientIp(ips, forwarded ? forwarded[1] : part); + }); + }); + + return ips; +} + +function responseText(res) { + if (!res) { + return ''; + } + + if (typeof res.text === 'function') { + return res.text(); + } + + if (typeof res.text === 'string') { + return res.text; + } + + if (typeof res.responseText === 'string') { + return res.responseText; + } + + if (res.xhr && typeof res.xhr.responseText === 'string') { + return res.xhr.responseText; + } + + return ''; +} + +function parseRemoteAddressResponse(text) { + var ips = parseClientIps(text); + + try { + var data = JSON.parse(text); + ['ipaddr', 'ip', 'remote_addr', 'address'].forEach(function (field) { + addClientIp(ips, data && data[field]); + }); + + L.toArray(data && data.ips).forEach(function (ip) { + addClientIp(ips, ip); + }); + } catch (e) { + } + + return Object.keys(ips); +} + +function normalizeClient(value) { + var ips = {}; + + L.toArray(value && value.ips).forEach(function (ip) { + addClientIp(ips, ip); + }); + + addClientIp(ips, value && value.ip); + + return { + ips: Object.keys(ips) + }; +} + +function appendCurrentClientIp(client, ip) { + if (!ip) { + return; + } + + client.ips = client.ips || []; + + if (client.ips.indexOf(ip) === -1) { + client.ips.push(ip); + } +} + +function requestRemoteAddress(path) { + return request.get(L.url(path)) + .then(function (res) { + if (res && typeof res.json === 'function') { + try { + var data = res.json(); + var jsonIps = {}; + + ['ipaddr', 'ip', 'remote_addr', 'address'].forEach(function (field) { + addClientIp(jsonIps, data && data[field]); + }); + + L.toArray(data && data.ips).forEach(function (ip) { + addClientIp(jsonIps, ip); + }); + + if (Object.keys(jsonIps).length > 0) { + return Object.keys(jsonIps); + } + } catch (e) { } + } + + return Promise.resolve(responseText(res)).then(function (text) { + return parseRemoteAddressResponse(text); + }); + }) + .catch(function () { + return null; + }); +} + +function getCurrentClient() { + var envIp = getEnvRemoteAddress(); + var client = { ips: [] }; + + if (envIp) { + appendCurrentClientIp(client, envIp); + } + + return requestRemoteAddress('admin/services/devgate/remote_addr') + .then(function (ips) { + L.toArray(ips).forEach(function (ip) { + appendCurrentClientIp(client, ip); + }); + + return normalizeClient(client); + }); +} + +function getHostEntries(hostHints) { + if (!hostHints) { + return {}; + } + + return hostHints.hosts || hostHints; +} + +function getHostHints() { + return L.resolveDefault(callHostHints(), {}); +} + +function normalizeHostIp(value) { + if (value && typeof value === 'object') { + value = value.address || value.ipaddr || value.ip || value.addr || value[0]; + } + + return normalizeIp(value); +} + +function getHostIps(host) { + var seen = {}; + var ips = []; + var fields = ['ipaddrs', 'ipaddr', 'ipv4', 'ipv4-address', 'ip6addrs', 'ip6addr', 'ipv6', 'ipv6-address']; + + fields.forEach(function (field) { + L.toArray(host && host[field]).forEach(function (value) { + var ip = normalizeHostIp(value); + + if (ip && !seen[ip]) { + seen[ip] = true; + ips.push(ip); + } + }); + }); + + return ips; +} + +function isIpv4(ip) { + return /^([0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ip); +} + +function ipv4ToInt(ip) { + var parts = String(ip || '').split('.'); + + if (parts.length !== 4) { + return null; + } + + var value = 0; + + for (var i = 0; i < parts.length; i++) { + var part = Number(parts[i]); + + if (!Number.isInteger(part) || part < 0 || part > 255) { + return null; + } + + value = (value << 8) + part; + } + + return value >>> 0; +} + +function maskToPrefix(mask) { + var value = ipv4ToInt(mask); + var prefix = 0; + var seenZero = false; + + if (value == null) { + return null; + } + + for (var i = 31; i >= 0; i--) { + if (value & (1 << i)) { + if (seenZero) { + return null; + } + + prefix++; + } else { + seenZero = true; + } + } + + return prefix; +} + +function getLanNetworks() { + var ipaddr = uci.get('network', 'lan', 'ipaddr'); + var netmask = uci.get('network', 'lan', 'netmask'); + var ipaddrs = L.toArray(ipaddr); + var networks = []; + + ipaddrs.forEach(function (value) { + var ip = String(value || '').trim(); + var prefix = null; + var slash = ip.indexOf('/'); + + if (slash !== -1) { + prefix = Number(ip.substring(slash + 1)); + ip = ip.substring(0, slash); + } else { + prefix = maskToPrefix(netmask || '255.255.255.0'); + } + + var addr = ipv4ToInt(ip); + + if (addr == null || !Number.isInteger(prefix) || prefix < 0 || prefix > 32) { + return; + } + + var mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0; + + networks.push({ + network: addr & mask, + mask: mask + }); + }); + + return networks; +} + +function isInNetworks(ip, networks) { + var addr = ipv4ToInt(ip); + + if (addr == null || networks.length === 0) { + return false; + } + + for (var i = 0; i < networks.length; i++) { + if ((addr & networks[i].mask) === networks[i].network) { + return true; + } + } + + return false; +} + +function getHostIpv4s(host) { + return getHostIps(host).filter(isIpv4); +} + +function formatHostChoice(mac, host, ipv4s) { + var normalizedMac = normalizeMac(mac) || String(mac || '').trim(); + var ipText = ipv4s.length > 0 ? ipv4s.join(', ') : _('未知IP'); + var name = String(host && host.name || '').trim() || _('未知设备'); + + return { + mac: normalizedMac, + name: name, + ipText: ipText + }; +} + +function formatHostSortText(mac, host, ipv4s) { + var normalizedMac = normalizeMac(mac) || String(mac || '').trim(); + var ipText = ipv4s.length > 0 ? ipv4s.join(', ') : _('未知IP'); + var name = String(host && host.name || '').trim() || _('未知设备'); + + return '%s %s %s'.format(name, normalizedMac.toUpperCase(), ipText); +} + +function buildHostChoices(hosts, lanNetworks, protectedClient) { + var hostChoices = []; + var hostChoiceByMac = {}; + + if (!hosts) { + return { + list: hostChoices, + byMac: hostChoiceByMac + }; + } + + Object.keys(hosts).forEach(function (mac) { + var host = hosts[mac]; + var ips = getHostIps(host); + var lanIpv4s = getHostIpv4s(host).filter(function (ip) { + return isInNetworks(ip, lanNetworks); + }); + var optionMac = normalizeMac(mac) || String(mac || '').trim(); + var choice; + + if (!optionMac || lanIpv4s.length === 0 || isProtectedHost(mac, ips, protectedClient)) { + return; + } + + choice = formatHostChoice(mac, host, lanIpv4s); + choice.sortText = formatHostSortText(mac, host, lanIpv4s); + + hostChoices.push(choice); + hostChoiceByMac[optionMac] = choice; + }); + + hostChoices.sort(function (a, b) { + return a.sortText.localeCompare(b.sortText); + }); + + return { + list: hostChoices, + byMac: hostChoiceByMac + }; +} + +function protectHost(protectedClient, mac, host) { + var normalizedMac = normalizeMac(mac); + var ips = getHostIps(host); + + if (normalizedMac) { + protectedClient.macs[normalizedMac] = true; + } + + for (var i = 0; i < ips.length; i++) { + protectedClient.ips[ips[i]] = true; + } +} + +function buildProtectedClient(hostHints, currentClient) { + var protectedClient = { + ips: {}, + macs: {} + }; + var hosts = getHostEntries(hostHints); + + L.toArray(currentClient && currentClient.ips).forEach(function (ip) { + protectedClient.ips[ip] = true; + }); + + if (hostHints && typeof hostHints.getMACAddrByIPAddr === 'function') { + L.toArray(currentClient && currentClient.ips).forEach(function (ip) { + var matchedMac = normalizeMac(hostHints.getMACAddrByIPAddr(ip)); + + if (matchedMac) { + if (hosts[matchedMac]) { + protectHost(protectedClient, matchedMac, hosts[matchedMac]); + } else { + protectedClient.macs[matchedMac] = true; + } + } + }); + } + + Object.keys(hosts).forEach(function (mac) { + var host = hosts[mac]; + var ips = getHostIps(host); + var isCurrentHost = false; + + for (var i = 0; i < ips.length; i++) { + if (protectedClient.ips[ips[i]]) { + isCurrentHost = true; + break; + } + } + + if (isCurrentHost) { + protectHost(protectedClient, mac, host); + } + }); + + return protectedClient; +} + +function isProtectedTarget(value, protectedClient) { + var ip = normalizeIp(value); + var mac = extractMac(value); + + return !!((ip && protectedClient.ips[ip]) || (mac && protectedClient.macs[mac])); +} + +function isProtectedHost(mac, ips, protectedClient) { + var normalizedMac = normalizeMac(mac); + + if (normalizedMac && protectedClient.macs[normalizedMac]) { + return true; + } + + for (var i = 0; i < ips.length; i++) { + if (protectedClient.ips[ips[i]]) { + return true; + } + } + + return false; +} + +function normalizeTimeRangeEndpoint(value) { + var match = String(value || '').trim().match(/^([0-9]{1,2}):([0-9]{1,2})$/); + var hour; + var minute; + + if (!match) { + return null; + } + + hour = Number(match[1]); + minute = Number(match[2]); + + if (hour > 24 || minute > 59 || (hour === 24 && minute !== 0)) { + return null; + } + + return '%02d:%02d'.format(hour, minute); +} + +function isTimeRangeEndpoint(value) { + return normalizeTimeRangeEndpoint(value) != null; +} + +function parseTimeRangesValue(value) { + var raw = String(value == null ? '' : value).trim(); + var ranges = []; + + if (raw === '') { + raw = DEV_GATE_DEFAULT_TIME_RANGE; + } + + raw.split(',').forEach(function (item) { + var match; + var start; + var end; + + if (ranges.length >= DEV_GATE_MAX_TIME_RANGES) { + return; + } + + item = String(item || '').trim(); + match = item.match(/^([^,-]+)-([^,-]+)$/); + start = match ? normalizeTimeRangeEndpoint(match[1]) : null; + end = match ? normalizeTimeRangeEndpoint(match[2]) : null; + + if (start && end) { + ranges.push({ + start: start, + end: end + }); + } + }); + + if (ranges.length === 0) { + ranges.push({ + start: '00:00', + end: '00:00' + }); + } + + return ranges; +} + +function emitTimeRangesChange(root) { + root.dispatchEvent(new CustomEvent('widget-change', { + bubbles: true, + detail: { + value: getTimeRangesInputValue(root) + } + })); + root.dispatchEvent(new Event('change', { bubbles: true })); +} + +function syncTimeRangeButtons(root) { + var rows = root.querySelectorAll('.devgate-time-range-row'); + var add = root.querySelector('.devgate-time-range-add'); + var readonly = root.getAttribute('data-readonly') === 'true'; + + if (add) { + add.disabled = readonly || rows.length >= DEV_GATE_MAX_TIME_RANGES; + } + + Array.prototype.forEach.call(root.querySelectorAll('.devgate-time-range-remove'), function (button) { + button.disabled = readonly || rows.length <= 1; + }); +} + +function parseTimeInputMinutes(value) { + var normalized = normalizeTimeRangeEndpoint(value); + var match = normalized ? normalized.match(/^([0-9]{2}):([0-9]{2})$/) : null; + var hour; + var minute; + + if (!match) { + return null; + } + + hour = Number(match[1]); + minute = Number(match[2]); + + return hour * 60 + minute; +} + +function formatTimeInputMinutes(minutes) { + var hour; + var minute; + + minutes = Math.max(0, Math.min(24 * 60, minutes)); + hour = Math.floor(minutes / 60); + minute = minutes % 60; + + return '%02d:%02d'.format(hour, minute); +} + +function normalizeTimeRangeInput(input) { + var value = normalizeTimeRangeEndpoint(input.value); + + if (value != null) { + input.value = value; + } +} + +function handleTimeInputWheel(root, input, ev) { + var current; + var step; + var direction; + + if (input.disabled || input.readOnly || document.activeElement !== input) { + return; + } + + current = parseTimeInputMinutes(input.value); + if (current == null) { + current = 0; + } + + step = ev.shiftKey ? 60 : 1; + direction = ev.deltaY < 0 ? 1 : -1; + input.value = formatTimeInputMinutes(current + direction * step); + + ev.preventDefault(); + input.dispatchEvent(new Event('input', { bubbles: true })); +} + +function bindTimeRangeInputEvents(root, input) { + function normalizeAndEmit() { + normalizeTimeRangeInput(input); + emitTimeRangesChange(root); + } + + input.addEventListener('input', function () { + emitTimeRangesChange(root); + }); + input.addEventListener('change', normalizeAndEmit); + input.addEventListener('blur', normalizeAndEmit); + input.addEventListener('wheel', function (ev) { + handleTimeInputWheel(root, input, ev); + }, { passive: false }); +} + +function appendTimeRangeRow(root, range) { + var cbid = root.getAttribute('data-cbid'); + var readonly = root.getAttribute('data-readonly') === 'true'; + var index = Number(root.getAttribute('data-next-index') || '0'); + var startId = index === 0 ? 'widget.' + cbid : 'widget.%s.%d.start'.format(cbid, index); + var endId = index === 0 ? 'widget.%s.end'.format(cbid) : 'widget.%s.%d.end'.format(cbid, index); + var start = E('input', { + id: startId, + type: 'text', + 'class': 'devgate-time-input', + inputmode: 'numeric', + maxlength: '5', + placeholder: '00:00', + value: range && range.start || '00:00', + disabled: readonly || null + }); + var end = E('input', { + id: endId, + type: 'text', + 'class': 'devgate-time-input', + inputmode: 'numeric', + maxlength: '5', + placeholder: '24:00', + value: range && range.end || '00:00', + disabled: readonly || null + }); + var action; + var row; + + root.setAttribute('data-next-index', String(index + 1)); + + bindTimeRangeInputEvents(root, start); + bindTimeRangeInputEvents(root, end); + + if (index === 0) { + action = E('input', { + type: 'button', + 'class': 'cbi-button devgate-time-range-action devgate-time-range-add', + value: _('添加'), + disabled: readonly || null, + click: function () { + if (root.querySelectorAll('.devgate-time-range-row').length >= DEV_GATE_MAX_TIME_RANGES) { + return; + } + + appendTimeRangeRow(root, { start: '00:00', end: '00:00' }); + syncTimeRangeButtons(root); + emitTimeRangesChange(root); + } + }); + } else { + action = E('input', { + type: 'button', + 'class': 'cbi-button devgate-time-range-action devgate-time-range-remove', + value: _('移除'), + disabled: readonly || null, + click: function () { + if (root.querySelectorAll('.devgate-time-range-row').length <= 1) { + return; + } + + row.parentNode.removeChild(row); + syncTimeRangeButtons(root); + emitTimeRangesChange(root); + } + }); + } + + row = E('div', { 'class': 'devgate-time-range-row' }, [ + start, + E('span', { 'class': 'devgate-time-separator' }, '-'), + end, + action + ]); + + root.appendChild(row); +} + +function renderTimeRanges(option, section_id, cfgvalue) { + var cbid = option.cbid(section_id); + var readonly = ((option.readonly != null) ? option.readonly : option.map.readonly) || null; + var root = E('div', { + id: cbid, + 'class': 'devgate-time-ranges', + 'data-cbid': cbid, + 'data-readonly': readonly ? 'true' : 'false' + }); + + parseTimeRangesValue(cfgvalue != null ? cfgvalue : option.default).forEach(function (range) { + appendTimeRangeRow(root, range); + }); + + syncTimeRangeButtons(root); + return root; +} + +function getTimeRangesInputValue(root) { + var values = []; + + Array.prototype.forEach.call(root.querySelectorAll('.devgate-time-range-row'), function (row) { + var inputs = row.querySelectorAll('.devgate-time-input'); + var start = inputs[0] ? String(inputs[0].value || '').trim() : ''; + var end = inputs[1] ? String(inputs[1].value || '').trim() : ''; + + start = normalizeTimeRangeEndpoint(start) || start; + end = normalizeTimeRangeEndpoint(end) || end; + values.push('%s-%s'.format(start, end)); + }); + + return values.join(','); +} + +function validateTimeRangesValue(value) { + var raw = String(value == null ? '' : value).trim(); + var ranges = raw ? raw.split(',') : []; + var timePattern = /^([^,]+)-([^,]+)$/; + var i; + var match; + + if (ranges.length < 1) { + return _('请至少保留一个禁用时段。'); + } + + if (ranges.length > DEV_GATE_MAX_TIME_RANGES) { + return _('禁用时段最多允许24个。'); + } + + for (i = 0; i < ranges.length; i++) { + match = String(ranges[i] || '').trim().match(timePattern); + if (!match || !normalizeTimeRangeEndpoint(match[1]) || !normalizeTimeRangeEndpoint(match[2])) { + return _('禁用时段范围必须为 00:00-24:00。'); + } + } + + return true; +} + +function parseWeekValue(value) { + var selected = {}; + var raw = String(value == null ? '' : value).trim(); + + if (raw === '' || raw === '0') { + WEEK_DAYS.forEach(function (day) { + selected[day.value] = true; + }); + return selected; + } + + raw.split(',').forEach(function (day) { + day = String(day || '').trim(); + if (day) { + selected[day] = true; + } + }); + + return selected; +} + +function getWeekInputValue(root) { + var values = []; + + WEEK_DAYS.forEach(function (day) { + var input = root.querySelector('input[value="%s"]'.format(day.value)); + + if (input && input.checked) { + values.push(day.value); + } + }); + + return values.length === WEEK_DAYS.length ? '0' : values.join(','); +} + +function renderWeekCheckboxes(option, section_id, cfgvalue) { + var selected = parseWeekValue(cfgvalue != null ? cfgvalue : option.default); + var cbid = option.cbid(section_id); + var root = E('div', { + id: cbid, + 'class': 'devgate-week-checkboxes' + }); + + WEEK_DAYS.forEach(function (day, index) { + var inputId = index === 0 ? 'widget.' + cbid : 'widget.%s.%d'.format(cbid, index); + var input = E('input', { + id: inputId, + type: 'checkbox', + value: day.value, + checked: selected[day.value] ? 'checked' : null, + disabled: ((option.readonly != null) ? option.readonly : option.map.readonly) || null + }); + + input.addEventListener('change', function (ev) { + if (!getWeekInputValue(root)) { + ev.target.checked = true; + } + }); + + root.appendChild(E('label', { 'class': 'devgate-week-checkbox' }, [ + input, + E('span', {}, day.label) + ])); + }); + + return root; +} + +function renderRuleModeRadios(option, section_id, cfgvalue) { + var cbid = option.cbid(section_id); + var value = String(cfgvalue != null ? cfgvalue : option.default || 'forward'); + var readonly = ((option.readonly != null) ? option.readonly : option.map.readonly) || null; + var choices = [ + { value: 'forward', label: _('公网') }, + { value: 'all', label: _('内网') } + ]; + + if (value === 'input') { + value = 'all'; + } + + return E('div', { + id: cbid, + 'class': 'devgate-mode-radios' + }, choices.map(function (choice, index) { + var inputId = index === 0 ? 'widget.' + cbid : 'widget.%s.%d'.format(cbid, index); + + return E('span', { 'class': 'cbi-radio' }, [ + E('input', { + id: inputId, + name: cbid, + type: 'radio', + 'class': 'cbi-input-radio', + value: choice.value, + checked: value === choice.value ? 'checked' : null, + disabled: readonly || null + }), + E('label', { 'for': inputId }), + E('span', {}, choice.label) + ]); + })); +} + +function renderRuleEnableButton(option, section_id, cfgvalue) { + var enabled = String(cfgvalue != null ? cfgvalue : option.default) !== '0'; + var readonly = ((option.readonly != null) ? option.readonly : option.map.readonly) || null; + var cbid = option.cbid(section_id); + var widgetId = 'widget.' + cbid; + var wrapper; + var input = E('input', { + id: widgetId, + type: 'checkbox', + 'class': 'devgate-rule-enable-input', + checked: enabled ? 'checked' : null, + disabled: readonly || null + }); + var button = E('label', { + 'for': widgetId, + 'class': 'devgate-rule-enable-button' + }); + + input.addEventListener('change', function () { + wrapper.setAttribute('data-changed', 'true'); + wrapper.dispatchEvent(new CustomEvent('widget-change', { + bubbles: true, + detail: { + value: input.checked ? '1' : '0' + } + })); + }); + + wrapper = E('div', { + id: cbid, + 'class': 'devgate-rule-enable-control' + }, [ + input, + button + ]); + + return wrapper; +} + +function bindDomOptionValidation(option) { + option._devgateValidationErrors = {}; + option.isValid = function (section_id) { + var result = (typeof this.validate === 'function') ? this.validate(section_id, this.formvalue(section_id)) : true; + + if (result === true) { + delete this._devgateValidationErrors[section_id]; + return true; + } + + this._devgateValidationErrors[section_id] = result || _('输入无效。'); + return false; + }; + option.getValidationError = function (section_id) { + return this._devgateValidationErrors[section_id] || ''; + }; +} + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('devgate'), + uci.load('network'), + getHostHints(), + getCurrentClient(), + getPackageVersion() + ]); + }, + + render: function (data) { + var m, s, o; + var hostHints = data[2]; + var hosts = getHostEntries(hostHints); + var protectedClient = buildProtectedClient(hostHints, data[3]); + var lanNetworks = getLanNetworks(); + var hostChoices = buildHostChoices(hosts, lanNetworks, protectedClient); + var usedRuleUids = {}; + var sectionRuleUids = {}; + + m = new form.Map('devgate'); + + var s = m.section(form.TypedSection, 'device', _('设备规则')); + s.addremove = true; + s.anonymous = true; + s.sortable = false; + s.addbtntitle = _('添加'); + s.delbtntitle = _('删除'); + s.renderContents = renderRulesContents; + + o = s.option(form.Value, 'comment', _('规则名称')); + o.optional = true; + o.placeholder = _('可选规则名称'); + + o = s.option(form.Value, 'uid', _('规则ID')); + o.rmempty = false; + o.renderWidget = function (section_id, option_index, cfgvalue) { + return E('input', { + id: this.cbid(section_id), + type: 'hidden', + value: resolveRuleUid(section_id, cfgvalue, usedRuleUids, sectionRuleUids) + }); + }; + o.formvalue = function (section_id) { + var elem = this.map.findElement('id', this.cbid(section_id)); + + return elem ? elem.value : null; + }; + o.write = function (section_id, value) { + return this.map.data.set( + this.uciconfig || this.map.config, + section_id, + this.option, + normalizeRuleUid(value) || resolveRuleUid(section_id, value, usedRuleUids, sectionRuleUids) + ); + }; + + o = s.option(form.Flag, 'enable', _('启用')); + o.rmempty = false; + o.default = '0'; + o.renderWidget = function (section_id, option_index, cfgvalue) { + return renderRuleEnableButton(this, section_id, cfgvalue); + }; + o.formvalue = function (section_id) { + var elem = this.map.findElement('id', this.cbid(section_id)); + var input = elem ? elem.querySelector('input[type="checkbox"]') : null; + + return input ? (input.checked ? '1' : '0') : null; + }; + o.write = function (section_id, value) { + return this.map.data.set(this.uciconfig || this.map.config, section_id, this.option, value === '0' ? '0' : '1'); + }; + + o = s.option(form.Value, 'mac', _('目标设备')); + o.rmempty = false; + // o.description = _('以MAC识别设备,IP仅用于辅助辨认。'); + o.renderWidget = function (section_id, option_index, cfgvalue) { + var cbid = this.cbid(section_id); + var mac = extractMac(cfgvalue); + var value = mac || String(cfgvalue || '').trim(); + var choice = mac ? hostChoices.byMac[mac] : null; + var disabled = ((this.readonly != null) ? this.readonly : this.map.readonly) || null; + var input = E('input', { + id: cbid, + type: 'hidden', + 'class': 'devgate-host-input', + value: value + }); + var display = E('span', { 'class': 'devgate-host-display' }); + var button = E('input', { + id: 'widget.' + cbid, + type: 'button', + 'class': 'devgate-host-select cbi-button', + 'aria-haspopup': 'listbox', + disabled: disabled || null, + click: function (ev) { + ev.preventDefault(); + openHostPicker(wrapper, button, input, extractMac(input.value)); + } + }); + var wrapper = E('div', { 'class': 'devgate-host-control' }, [ + button, + display, + input + ]); + + syncHostDisplay(wrapper, choice); + + return wrapper; + }; + o.formvalue = function (section_id) { + var elem = this.map.findElement('id', this.cbid(section_id)); + + return elem ? elem.value : null; + }; + o.write = function (section_id, value) { + var mac = extractMac(value) || String(value || '').trim(); + + return this.map.data.set(this.uciconfig || this.map.config, section_id, this.option, mac); + }; + o.validate = function (section_id, value) { + if (isProtectedTarget(value, protectedClient)) { + return _('不能选择当前登录设备。'); + } + + if (String(value || '').trim() !== '' && !extractMac(value)) { + return _('请选择或输入设备MAC。'); + } + + return true; + }; + bindDomOptionValidation(o); + + o = s.option(form.ListValue, 'chain', _('管控方式')); + o.value('forward', _('公网')); + o.value('all', _('内网')); + o.default = 'forward'; + o.rmempty = false; + o.cfgvalue = function (section_id) { + var value = this.map.data.get(this.uciconfig || this.map.config, section_id, this.option); + + return value === 'input' ? 'all' : value; + }; + o.renderWidget = function (section_id, option_index, cfgvalue) { + return renderRuleModeRadios(this, section_id, cfgvalue); + }; + o.formvalue = function (section_id) { + var elem = this.map.findElement('id', this.cbid(section_id)); + var checked = elem ? elem.querySelector('input[type="radio"]:checked') : null; + + return checked ? checked.value : null; + }; + o.write = function (section_id, value) { + return this.map.data.set(this.uciconfig || this.map.config, section_id, this.option, value === 'input' ? 'all' : value); + }; + + o = s.option(form.Value, 'time_ranges', _('禁用时段')); + o.default = DEV_GATE_DEFAULT_TIME_RANGE; + o.rmempty = false; + o.renderWidget = function (section_id, option_index, cfgvalue) { + return renderTimeRanges(this, section_id, cfgvalue); + }; + o.formvalue = function (section_id) { + var elem = this.map.findElement('id', this.cbid(section_id)); + + return elem ? getTimeRangesInputValue(elem) : null; + }; + o.write = function (section_id, value) { + return this.map.data.set(this.uciconfig || this.map.config, section_id, this.option, value || DEV_GATE_DEFAULT_TIME_RANGE); + }; + o.validate = function (section_id, value) { + return validateTimeRangesValue(value); + }; + bindDomOptionValidation(o); + + o = s.option(form.Value, 'week', _('生效周期')); + o.default = '0'; + o.rmempty = false; + o.renderWidget = function (section_id, option_index, cfgvalue) { + return renderWeekCheckboxes(this, section_id, cfgvalue); + }; + o.formvalue = function (section_id) { + var elem = this.map.findElement('id', this.cbid(section_id)); + + return elem ? getWeekInputValue(elem) : null; + }; + o.write = function (section_id, value) { + return this.map.data.set(this.uciconfig || this.map.config, section_id, this.option, value || '0'); + }; + o.validate = function (section_id, value) { + return value ? true : _('请至少选择一天。'); + }; + bindDomOptionValidation(o); + // o.description = _('规则生效的日期。'); + + return m.render().then(function (mapEl) { + var pageEl = E('div', { 'class': 'devgate-page' }, [ + renderStylesheet(data[4]), + renderHostPicker(hostChoices.list), + renderPageHeader(data[4]), + mapEl + ]); + + (async () => { + function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + // 计算容器外部元素占用高度 + var pageOffset = 0; + var offsetSelectors = ['.bg-primary', '.mobile-hide', '.cbi-page-actions']; + while (!document.querySelector('.cbi-page-actions')) await sleep(100); + for (let selector of offsetSelectors) { + let node = document.querySelector(selector); + if (node) { + let style = window.getComputedStyle(node); + if (style.display === 'none' || style.visibility === 'hidden') continue; + let rect = node.getBoundingClientRect(); + pageOffset += rect.height + (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0); + } + } + let container = document.querySelector('#maincontent>.container'); + if (container) { + let style = window.getComputedStyle(container); + pageOffset += (parseFloat(style.marginTop) || 0) + (parseFloat(style.marginBottom) || 0); + } + pageOffset = Math.ceil(pageOffset); + if (pageOffset > 0) { + pageEl.style.setProperty('--devgate-page-offset', '%dpx'.format(pageOffset)); + } + })(); + + watchRuleEnableStates(mapEl); + + return pageEl; + }); + } +}); diff --git a/luci-app-devgate/root/etc/init.d/devgate b/luci-app-devgate/root/etc/init.d/devgate new file mode 100755 index 000000000..66a964e50 --- /dev/null +++ b/luci-app-devgate/root/etc/init.d/devgate @@ -0,0 +1,31 @@ +#!/bin/sh /etc/rc.common + +START=99 +USE_PROCD=1 + +NAME=devgate + +start_service() { + procd_open_instance + procd_set_param command /usr/bin/devgatectrl + procd_set_param respawn + procd_set_param stderr 1 + procd_close_instance +} + +service_triggers() { + procd_add_reload_trigger 'devgate' +} + +reload_service() { + touch "/var/$NAME.reload" + pidof devgatectrl >/dev/null 2>&1 || start_service + return 0 +} + +stop_service(){ + local pids + pids="$(pidof devgatectrl 2>/dev/null)" + [ -n "$pids" ] && kill $pids >/dev/null 2>&1 + [ -x /usr/bin/devgate ] && /usr/bin/devgate stop || true +} diff --git a/luci-app-devgate/root/etc/uci-defaults/luci-devgate b/luci-app-devgate/root/etc/uci-defaults/luci-devgate new file mode 100644 index 000000000..d081ca89a --- /dev/null +++ b/luci-app-devgate/root/etc/uci-defaults/luci-devgate @@ -0,0 +1,33 @@ +#!/bin/sh +[ ! -f "/usr/share/ucitrack/luci-app-devgate.json" ] && { + cat > /usr/share/ucitrack/luci-app-devgate.json << EEOF +{ + "config": "devgate", + "init": "devgate" +} +EEOF +} +chmod +x /etc/init.d/devgate /usr/bin/devgate* +uci -q batch <<-EOF >/dev/null + delete ucitrack.@devgate[-1] + add ucitrack devgate + set ucitrack.@devgate[-1].init=devgate + commit ucitrack +EOF + +[ -e /etc/config/devgate ] || cat > /etc/config/devgate <<'EOF' +config device + option enable '0' + option mac '' + option time_ranges '00:00-00:00' + option week '0' + option chain 'forward' +EOF +rm -f /etc/config/devgate-opkg + +/etc/init.d/devgate enable >/dev/null 2>&1 || true +/etc/init.d/devgate start >/dev/null 2>&1 || true + +rm -rf /tmp/luci-indexcache /tmp/luci-modulecache /tmp/luci-*cache +/etc/init.d/rpcd reload >/dev/null 2>&1 || true +exit 0 diff --git a/luci-app-devgate/root/usr/bin/devgate b/luci-app-devgate/root/usr/bin/devgate new file mode 100755 index 000000000..8157e7999 --- /dev/null +++ b/luci-app-devgate/root/usr/bin/devgate @@ -0,0 +1,617 @@ +#!/bin/sh + +crrun=$1 +crid=$2 +crstate=${3:-1} +NAME=devgate +DEBUG=1 # 开启调试 + +LOG_FILE="/var/log/devgate.log" +IDLIST="/var/$NAME.idlist" +RULE_PREFIX="devgate_" +DEFAULT_SRC_ZONE="lan" +PUBLIC_DEST_ZONE="wan" +INTERNAL_DEST_ZONE="*" + +bin_fw4=$(command -v fw4 2>/dev/null) + +config_t_get() { + local index=${3:-0} + local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}") + echo "${ret:-$4}" +} + +normalize_rule_uid() { + local uid + uid=$(printf '%s' "${1:-}" | tr 'A-F' 'a-f' | sed 's/[^a-z0-9]//g' | cut -c1-6) + + if echo "$uid" | grep -qE '^[a-z0-9]{6}$'; then + echo "$uid" + return 0 + fi + + return 1 +} + +get_rule_uid() { + local id="$1" + local uid + + uid="$(normalize_rule_uid "$(config_t_get device uid "$id")")" || uid="" + [ -n "$uid" ] || return 1 + + echo "$uid" +} + +resolve_rule_ref() { + local ref="$1" + local uid + + if echo "$ref" | grep -qE '^[0-9]+$' && uci -q show "${NAME}.@device[$ref]" >/dev/null 2>&1; then + get_rule_uid "$ref" + return $? + fi + + normalize_rule_uid "$ref" +} + +get_device_mode() { + local id="$1" + local device_mode=$(config_t_get device chain "$id" "forward") + + case "$device_mode" in + all|input) + echo "all" + ;; + *) + echo "forward" + ;; + esac +} + +get_forward_dest_zone() { + local device_mode="$1" + + case "$device_mode" in + all) + echo "$INTERNAL_DEST_ZONE" + ;; + *) + echo "$PUBLIC_DEST_ZONE" + ;; + esac +} + +normalize_enabled() { + case "${1:-1}" in + 1|true|yes|on|enabled) + echo "1" + ;; + *) + echo "0" + ;; + esac +} + +dbg() { + if [ "$DEBUG" -eq 1 ]; then + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] FW-DEBUG: $@" >> "$LOG_FILE" + fi +} + +info() { + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] FW-INFO: $@" >> "$LOG_FILE" +} + +run_logged() { + local label="$1" + shift + + local output rc + output="$("$@" 2>&1)" + rc=$? + + [ -n "$output" ] && { + while IFS= read -r line; do + dbg "$label: $line" + done <<-EOF + $output + EOF + } + + return "$rc" +} + +normalize_mac() { + local mac + mac=$(echo "${1:-}" | xargs | tr 'A-F' 'a-f') + + if echo "$mac" | grep -qE '^([0-9a-f]{2}:){5}[0-9a-f]{2}$'; then + echo "$mac" + return 0 + fi + + return 1 +} + +rule_section() { + local uid="$1" + + echo "${RULE_PREFIX}${uid}" +} + +rule_name() { + local uid="$1" + + echo "DevGate-$uid" +} + +input_rule_name() { + local uid="$1" + + echo "DevGate-$uid-Input" +} + +check_firewall_tool() { + if ! command -v uci >/dev/null 2>&1; then + info "错误: 未找到uci命令" + return 1 + fi + + if [ ! -x /etc/init.d/firewall ] && [ ! -x "$bin_fw4" ]; then + info "错误: 未找到firewall4防火墙服务" + return 1 + fi + + dbg "检测到OpenWrt firewall UCI接口" + [ -x /etc/init.d/firewall ] && dbg "firewall服务: /etc/init.d/firewall" + [ -x "$bin_fw4" ] && dbg "fw4命令: $bin_fw4" + + return 0 +} + +reload_firewall() { + if [ -x /etc/init.d/firewall ]; then + run_logged "firewall reload" /etc/init.d/firewall reload + return $? + fi + + run_logged "fw4 reload" "$bin_fw4" reload +} + +commit_firewall() { + uci -q commit firewall || { + info "错误: 提交firewall配置失败" + return 1 + } + + if [ "${DEVGATE_DEFER_FIREWALL_RELOAD:-0}" = "1" ]; then + dbg "延迟firewall重载" + return 0 + fi + + reload_firewall || { + info "错误: 重载firewall失败" + return 1 + } +} + +delete_device_firewall_rules() { + local ref="$1" + local uid + + uid="$(resolve_rule_ref "$ref")" || uid="" + + [ -n "$uid" ] && { + uci -q delete "firewall.$(rule_section "$uid")" + uci -q delete "firewall.$(rule_section "$uid")_forward" + uci -q delete "firewall.$(rule_section "$uid")_input" + } + + uci -q delete "firewall.${RULE_PREFIX}${ref}_forward" + uci -q delete "firewall.${RULE_PREFIX}${ref}_input" +} + +delete_all_devgate_firewall_rules() { + local section + + uci -q show firewall | sed -n "s/^firewall\.\(${RULE_PREFIX}[A-Za-z0-9_]*\)=rule$/\1/p" | while IFS= read -r section; do + [ -n "$section" ] && uci -q delete "firewall.$section" + done +} + +set_firewall_option() { + local section="$1" + local option="$2" + local value="$3" + + uci -q set "firewall.$section.$option=$value" +} + +create_forward_rule() { + local section="$1" + local mac="$2" + local name="$3" + local dest="$4" + local enabled + + enabled="$(normalize_enabled "$5")" + + uci -q set "firewall.$section=rule" || return 1 + set_firewall_option "$section" enabled "$enabled" || return 1 + set_firewall_option "$section" name "$name" || return 1 + set_firewall_option "$section" src "$DEFAULT_SRC_ZONE" || return 1 + set_firewall_option "$section" dest "$dest" || return 1 + set_firewall_option "$section" src_mac "$mac" || return 1 + set_firewall_option "$section" proto "all" || return 1 + set_firewall_option "$section" target "REJECT" || return 1 +} + +create_input_rule() { + local section="$1" + local mac="$2" + local name="$3" + local enabled + + enabled="$(normalize_enabled "$4")" + + uci -q set "firewall.$section=rule" || return 1 + set_firewall_option "$section" enabled "$enabled" || return 1 + set_firewall_option "$section" name "$name" || return 1 + set_firewall_option "$section" src "$DEFAULT_SRC_ZONE" || return 1 + set_firewall_option "$section" src_mac "$mac" || return 1 + set_firewall_option "$section" proto "all" || return 1 + set_firewall_option "$section" target "REJECT" || return 1 +} + +verify_firewall_rule() { + local uid="$1" + local mac="$2" + local device_mode="$3" + local expected_enabled="${4:-}" + local section input_section + local expected_dest + + section=$(rule_section "$uid") + input_section="${section}_input" + expected_dest=$(get_forward_dest_zone "$device_mode") + + dbg "验证firewall通信规则: UID=$uid MAC=$mac mode=$device_mode dest=$expected_dest" + + [ "$(uci -q get "firewall.$section.name")" = "$(rule_name "$uid")" ] || { + dbg "验证失败: 规则名称不是$(rule_name "$uid")" + return 1 + } + + [ "$(uci -q get "firewall.$section.src_mac")" = "$mac" ] || { + dbg "验证失败: 转发规则不存在或MAC不匹配" + return 1 + } + + [ "$(uci -q get "firewall.$section.target")" = "REJECT" ] || { + dbg "验证失败: 转发规则目标不是REJECT" + return 1 + } + + [ "$(uci -q get "firewall.$section.dest")" = "$expected_dest" ] || { + dbg "验证失败: 转发规则目标区域不是$expected_dest" + return 1 + } + + if [ -n "$expected_enabled" ]; then + [ "$(uci -q get "firewall.$section.enabled")" = "$expected_enabled" ] || { + dbg "验证失败: 转发规则启用状态不是$expected_enabled" + return 1 + } + fi + + if [ "$device_mode" = "all" ]; then + [ "$(uci -q get "firewall.$input_section.name")" = "$(input_rule_name "$uid")" ] || { + dbg "验证失败: 本机访问规则名称不是$(input_rule_name "$uid")" + return 1 + } + + [ "$(uci -q get "firewall.$input_section.src_mac")" = "$mac" ] || { + dbg "验证失败: 本机访问规则不存在或MAC不匹配" + return 1 + } + + [ "$(uci -q get "firewall.$input_section.target")" = "REJECT" ] || { + dbg "验证失败: 本机访问规则目标不是REJECT" + return 1 + } + + if [ -n "$expected_enabled" ]; then + [ "$(uci -q get "firewall.$input_section.enabled")" = "$expected_enabled" ] || { + dbg "验证失败: 本机访问规则启用状态不是$expected_enabled" + return 1 + } + fi + else + ! uci -q show "firewall.$input_section" >/dev/null 2>&1 || { + dbg "验证失败: 存在过期本机访问规则" + return 1 + } + fi + + dbg "验证成功: DevGate通信规则已写入firewall配置" + return 0 +} + +init_firewall() { + check_firewall_tool || return 1 + + info "初始化防火墙规则 (黑名单模式,REJECT目标,规则同步到firewall通信规则)" + delete_all_devgate_firewall_rules + commit_firewall || return 1 + rm -f "$IDLIST" + info "firewall通信规则初始化完成" + + return 0 +} + +stop_firewall() { + info "停止防火墙规则" + check_firewall_tool || return 1 + + delete_all_devgate_firewall_rules + commit_firewall || return 1 + + rm -f "$IDLIST" + info "DevGate通信规则已删除" +} + +disable_device_config() { + local id="$1" + + uci -q set "${NAME}.@device[$id].enable=0" || return 1 + uci -q commit "$NAME" || return 1 +} + +skip_device_without_mac() { + local id="$1" + local reason="$2" + local uid + + info "$reason,固定为未启用: ID=$id" + uid="$(get_rule_uid "$id" 2>/dev/null)" || uid="" + [ -n "$uid" ] && delete_device_firewall_rules "$uid" + disable_device_config "$id" || return 1 + commit_firewall || return 1 +} + +add_device() { + check_firewall_tool || return 1 + + local id="$1" + local rule_enabled + local uid + local target=$(config_t_get device mac "$id") + local device_mode=$(get_device_mode "$id") + local dest_zone + [ -z "$target" ] && { + skip_device_without_mac "$id" "目标设备为空" || return 1 + return 0 + } + + local comment=$(config_t_get device comment "$id" "设备$id") + local mac + + mac=$(normalize_mac "$target") || { + skip_device_without_mac "$id" "目标MAC无效: $target" || return 1 + return 0 + } + rule_enabled="$(normalize_enabled "$2")" + dest_zone=$(get_forward_dest_zone "$device_mode") + uid="$(get_rule_uid "$id")" || { + info "添加失败: 规则唯一识别码缺失 ID=$id" + return 1 + } + + info "添加设备到firewall通信规则: $comment ($mac, uid=$uid, $device_mode, dest=$dest_zone, enabled=$rule_enabled)" + + delete_device_firewall_rules "$uid" + delete_device_firewall_rules "$id" + create_forward_rule "$(rule_section "$uid")" "$mac" "$(rule_name "$uid")" "$dest_zone" "$rule_enabled" || return 1 + if [ "$device_mode" = "all" ]; then + create_input_rule "$(rule_section "$uid")_input" "$mac" "$(input_rule_name "$uid")" "$rule_enabled" || return 1 + fi + + if ! commit_firewall; then + info "添加失败: firewall重载失败,回滚DevGate规则" + delete_device_firewall_rules "$uid" + commit_firewall >/dev/null 2>&1 || true + return 1 + fi + + verify_firewall_rule "$uid" "$mac" "$device_mode" "$rule_enabled" || return 1 +} + +del_device() { + check_firewall_tool || return 1 + + local ref="$1" + local uid + local target="" + local comment="规则$ref" + + uid="$(resolve_rule_ref "$ref")" || { + info "移除失败: 规则唯一识别码无效 $ref" + return 1 + } + + if echo "$ref" | grep -qE '^[0-9]+$' && uci -q show "${NAME}.@device[$ref]" >/dev/null 2>&1; then + target=$(config_t_get device mac "$ref") + comment=$(config_t_get device comment "$ref" "设备$ref") + fi + + info "从firewall通信规则移除设备: $comment ($target, uid=$uid)" + + delete_device_firewall_rules "$uid" + delete_device_firewall_rules "$ref" + commit_firewall || return 1 +} + +show_firewall_status() { + check_firewall_tool || return 1 + + echo "" + echo "防火墙状态:" + echo "控制方式: 黑名单" + echo "控制强度: 按设备规则" + echo "规则来源: /etc/config/firewall 通信规则" + echo "" + + echo "DevGate通信规则:" + if uci -q show firewall | grep -q "^firewall\.${RULE_PREFIX}"; then + uci -q show firewall | grep "^firewall\.${RULE_PREFIX}" + else + echo " 当前没有DevGate通信规则" + fi +} + +diagnose() { + echo "" + echo "=== 设备门禁系统诊断 ===" + echo "" + + echo "1. 服务状态:" + if pidof devgatectrl >/dev/null 2>&1; then + echo " ✓ devgatectrl 正在运行" + else + echo " ✗ devgatectrl 未运行" + fi + + echo "" + echo "2. 配置文件:" + if [ -f "/etc/config/devgate" ]; then + echo " ✓ 配置文件存在" + uci show devgate 2>/dev/null | grep -c "device" | while read count; do + echo " 配置了 $count 个设备" + done + else + echo " ✗ 配置文件不存在" + fi + + echo "" + echo "3. 防火墙接口:" + if command -v uci >/dev/null 2>&1; then + echo " ✓ uci: $(command -v uci)" + else + echo " ✗ 未找到uci" + fi + + if [ -x /etc/init.d/firewall ]; then + echo " ✓ firewall服务: /etc/init.d/firewall" + else + echo " ✗ 未找到firewall服务" + fi + + if [ -x "$bin_fw4" ]; then + echo " ✓ fw4: $bin_fw4" + echo " 版本: $($bin_fw4 -V 2>/dev/null | head -1)" + else + echo " ? 未找到fw4命令" + fi + + show_firewall_status + + echo "" + echo "4. 当前控制列表:" + if [ -f "$IDLIST" ] && [ -s "$IDLIST" ]; then + echo " 当前禁止的设备:" + cat "$IDLIST" | sed 's/!//g' | while read ref; do + echo " $ref" + done + else + echo " 当前没有设备被禁止" + fi + + echo "" + echo "=== 诊断完成 ===" +} + +case "$crrun" in + start) + info "启动设备门禁" + init_firewall + if [ $? -eq 0 ]; then + info "设备门禁启动成功" + show_firewall_status + else + info "设备门禁启动失败" + exit 1 + fi + ;; + + stop) + info "停止设备门禁" + stop_firewall + info "设备门禁已停止" + ;; + + add) + [ -z "$crid" ] && { + echo "错误: 需要指定设备ID" + exit 1 + } + info "添加设备控制: ID=$crid enabled=$(normalize_enabled "$crstate")" + add_device "$crid" "$crstate" || exit 1 + show_firewall_status + ;; + + uid) + [ -z "$crid" ] && { + echo "错误: 需要指定设备ID" + exit 1 + } + get_rule_uid "$crid" || exit 1 + ;; + + del) + [ -z "$crid" ] && { + echo "错误: 需要指定设备ID" + exit 1 + } + info "移除设备控制: ID=$crid" + del_device "$crid" || exit 1 + show_firewall_status + ;; + + status) + show_firewall_status + ;; + + reload) + check_firewall_tool || exit 1 + reload_firewall || exit 1 + ;; + + diagnose) + diagnose + ;; + + help|"") + echo "设备门禁系统命令工具" + echo "" + echo "用法: $0 {start|stop|add |del |status|diagnose|help}" + echo "" + echo "命令说明:" + echo " start - 初始化firewall通信规则" + echo " stop - 停止并清理所有DevGate通信规则" + echo " add [0|1] - 添加设备到控制列表,并设置防火墙规则启用状态" + echo " del - 从控制列表移除设备" + echo " status - 显示防火墙状态" + echo " reload - 重载firewall服务" + echo " diagnose - 系统诊断" + echo " help - 显示此帮助信息" + ;; + + *) + echo "错误: 未知命令 '$crrun'" + echo "使用: $0 help 查看帮助" + exit 1 + ;; +esac diff --git a/luci-app-devgate/root/usr/bin/devgatectrl b/luci-app-devgate/root/usr/bin/devgatectrl new file mode 100755 index 000000000..5cbf28777 --- /dev/null +++ b/luci-app-devgate/root/usr/bin/devgatectrl @@ -0,0 +1,427 @@ +#!/bin/sh + +NAME=devgate +LOG_FILE="/var/log/devgate.log" +DEBUG=1 + +IDLIST="/var/$NAME.idlist" +RELOAD_FLAG="/var/$NAME.reload" +FIREWALL_DIRTY=0 +NO_ENABLED_CLEARED=0 + +# 日志函数 +dbg() { + [ "$DEBUG" -eq 1 ] && { + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] CTRL-DEBUG: $@" >> "$LOG_FILE" + } +} + +info() { + local d="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$d] CTRL-INFO: $@" >> "$LOG_FILE" +} + +# 配置文件读取 +config_t_get() { + local index=${3:-0} + local ret=$(uci -q get "${NAME}.@${1}[${index}].${2}") + echo "${ret:=${4}}" +} + +normalize_mac() { + local mac + mac=$(echo "${1:-}" | xargs | tr 'A-F' 'a-f') + + if echo "$mac" | grep -qE '^([0-9a-f]{2}:){5}[0-9a-f]{2}$'; then + echo "$mac" + return 0 + fi + + return 1 +} + +# 获取启用设备 +get_enabled_devices() { + uci show "$NAME" 2>/dev/null | sed -n "s/^${NAME}\.@device\[\([0-9][0-9]*\)\]\.enable='1'.*/\1/p" | sort -n +} + +get_enabled_device_count() { + get_enabled_devices | wc -l +} + +is_enabled_device() { + local id="$1" + + get_enabled_devices | grep -qx "$id" +} + +get_device_uid() { + local id="$1" + local uid + + uid="$(devgate uid "$id" 2>/dev/null)" || return 1 + printf '%s\n' "$uid" +} + +firewall_rule_section() { + local uid="$1" + + echo "devgate_$uid" +} + +firewall_rule_exists() { + local uid="$1" + + [ -n "$uid" ] && uci -q show "firewall.$(firewall_rule_section "$uid")" >/dev/null 2>&1 +} + +get_firewall_rule_enabled() { + local uid="$1" + local value + + value="$(uci -q get "firewall.$(firewall_rule_section "$uid").enabled")" + echo "${value:-0}" +} + +remember_managed_uid() { + local uid="$1" + + [ -n "$uid" ] || return 0 + mkdir -p "$(dirname "$IDLIST")" 2>/dev/null + touch "$IDLIST" 2>/dev/null || return 1 + if ! grep -q "!$uid!" "$IDLIST" 2>/dev/null; then + echo "!$uid!" >> "$IDLIST" + fi + sort -u "$IDLIST" -o "$IDLIST" 2>/dev/null || true +} + +forget_managed_uid() { + local uid="$1" + + [ -n "$uid" ] || return 0 + sed -i "/!$uid!/d" "$IDLIST" 2>/dev/null +} + +is_managed_uid() { + local uid="$1" + + [ -f "$IDLIST" ] && grep -q "!${uid}!" "$IDLIST" 2>/dev/null +} + +disable_device_rule() { + local id="$1" + local uid="$2" + local target=$(config_t_get device mac "$id") + local comment=$(config_t_get device comment "$id" "设备$id") + + info "停用DevGate规则: $comment ($target, uid=$uid)" + uci -q set "${NAME}.@device[$id].enable=0" || return 1 + uci -q commit "$NAME" || return 1 + forget_managed_uid "$uid" + return 0 +} + +disable_device_without_mac() { + local id="$1" + local uid="$2" + local target=$(config_t_get device mac "$id") + local comment=$(config_t_get device comment "$id" "设备$id") + + info "目标设备为空或无效,强制未启用: $comment ($target, uid=$uid)" + run_devgate_change del "$uid" + disable_device_rule "$id" "$uid" +} + +is_enabled_uid() { + local uid="$1" + local id + + for id in $(get_enabled_devices); do + [ "$(get_device_uid "$id")" = "$uid" ] && return 0 + done + + return 1 +} + +run_devgate_change() { + local rc + + DEVGATE_DEFER_FIREWALL_RELOAD=1 devgate "$@" + rc=$? + + [ "$rc" -eq 0 ] && FIREWALL_DIRTY=1 + return "$rc" +} + +flush_firewall_changes() { + [ "$FIREWALL_DIRTY" -eq 1 ] || return 0 + + if devgate reload; then + FIREWALL_DIRTY=0 + return 0 + fi + + info "firewall重载失败" + return 1 +} + +# 时间检查函数 +time_to_minutes() { + local value="$1" + local hour="${value%:*}" + local minute="${value#*:}" + + hour="$(echo "$hour" | sed 's/^0*//')" + minute="$(echo "$minute" | sed 's/^0*//')" + [ -n "$hour" ] || hour=0 + [ -n "$minute" ] || minute=0 + + echo $((hour * 60 + minute)) +} + +is_time_value() { + local value="${1:-}" + local hour="${value%:*}" + local minute="${value#*:}" + + [ "$hour:$minute" = "$value" ] || return 1 + echo "$hour" | grep -qE '^[0-9]{1,2}$' || return 1 + echo "$minute" | grep -qE '^[0-9]{1,2}$' || return 1 + + hour="$(echo "$hour" | sed 's/^0*//')" + minute="$(echo "$minute" | sed 's/^0*//')" + [ -n "$hour" ] || hour=0 + [ -n "$minute" ] || minute=0 + + [ "$hour" -ge 0 ] && [ "$hour" -le 24 ] || return 1 + [ "$minute" -ge 0 ] && [ "$minute" -le 59 ] || return 1 + [ "$hour" -lt 24 ] || [ "$minute" -eq 0 ] +} + +is_time_in_range() { + local start_time=$1 + local end_time=$2 + local current_time=$(date +%H:%M) + local start_min + local end_min + local current_min + + start_min=$(time_to_minutes "$start_time") + end_min=$(time_to_minutes "$end_time") + current_min=$(time_to_minutes "$current_time") + + if [ "$start_min" -eq "$end_min" ]; then + return 0 + elif [ "$start_min" -lt "$end_min" ]; then + [ "$current_min" -ge "$start_min" ] && [ "$current_min" -lt "$end_min" ] && return 0 + else + [ "$current_min" -ge "$start_min" ] || [ "$current_min" -lt "$end_min" ] && return 0 + fi + + return 1 +} + +is_time_ranges_active() { + local time_ranges="${1:-00:00-00:00}" + local range + local start_time + local end_time + + for range in $(printf '%s' "$time_ranges" | sed 's/,/ /g'); do + range=$(echo "$range" | xargs) + [ -n "$range" ] || continue + + start_time="${range%-*}" + end_time="${range#*-}" + + if [ "$start_time" = "$range" ] || [ "$end_time" = "$range" ]; then + dbg "跳过无效禁用时段: $range" + continue + fi + + if ! is_time_value "$start_time" || ! is_time_value "$end_time"; then + dbg "跳过无效禁用时段: $range" + continue + fi + + is_time_in_range "$start_time" "$end_time" && return 0 + done + + return 1 +} + +is_weekday_in_range() { + local configured_weekdays=$1 + local current_weekday=$(date +%u) + + [ "$configured_weekdays" = "0" ] && return 0 + + for ww in $(echo "$configured_weekdays" | sed 's/,/ /g'); do + [ "$current_weekday" = "$ww" ] && return 0 + done + return 1 +} + +# 主检查函数 +check_device_control() { + local id="$1" + local target=$(config_t_get device mac "$id") + local weekdays=$(config_t_get device week "$id" "0") + local time_ranges="$(config_t_get device time_ranges "$id" "00:00-00:00")" + + is_weekday_in_range "$weekdays" || { + dbg "生效日期外: $target" + return 0 + } + + if is_time_ranges_active "$time_ranges"; then + dbg "禁用时段内: $target ($time_ranges)" + return 1 + fi + + dbg "禁用时段外: $target ($time_ranges)" + return 0 +} + +# 同步设备状态 +sync_device_status() { + local id="$1" + local force_refresh="${2:-0}" + local uid + local target=$(config_t_get device mac "$id") + local comment=$(config_t_get device comment "$id" "设备$id") + local mac + local should_block=0 + local firewall_exists=0 + local firewall_enabled + + uid="$(get_device_uid "$id")" || { + info "规则唯一识别码缺失,停用DevGate规则: 设备$id" + uci -q set "${NAME}.@device[$id].enable=0" || return 1 + uci -q commit "$NAME" || return 1 + return 1 + } + + mac="$(normalize_mac "$target")" || { + disable_device_without_mac "$id" "$uid" + return 0 + } + + if firewall_rule_exists "$uid"; then + firewall_exists=1 + elif is_managed_uid "$uid"; then + disable_device_rule "$id" "$uid" + return 0 + fi + + if ! check_device_control "$id"; then + should_block=1 + fi + + dbg "设备状态: $mac, uid=$uid, 防火墙规则存在=$firewall_exists, 应阻止=$should_block" + + if [ "$firewall_exists" -eq 0 ]; then + dbg "创建托管防火墙规则: $target enabled=$should_block" + if run_devgate_change add "$id" "$should_block"; then + remember_managed_uid "$uid" + if [ "$should_block" -eq 1 ]; then + info "阻止上网: $comment ($target)" + else + info "规则待命: $comment ($target)" + fi + else + info "创建防火墙规则失败: $comment ($target)" + fi + return 0 + fi + + firewall_enabled="$(get_firewall_rule_enabled "$uid")" + if [ "$firewall_enabled" != "$should_block" ] || [ "$force_refresh" -eq 1 ]; then + if [ "$should_block" -eq 1 ]; then + dbg "启用防火墙阻止: $target" + else + dbg "停用防火墙阻止: $target" + fi + run_devgate_change add "$id" "$should_block" + fi + remember_managed_uid "$uid" + + if [ "$should_block" -eq 1 ]; then + info "阻止上网: $comment ($target)" + else + info "允许上网: $comment ($target)" + fi +} + +sync_removed_devices() { + local ids id tmp + + [ -f "$IDLIST" ] || return 0 + + tmp="${IDLIST}.$$" + : > "$tmp" + + ids=$(sed -n 's/^!\([A-Za-z0-9][A-Za-z0-9]*\)!$/\1/p' "$IDLIST" 2>/dev/null | sort | uniq) + for id in $ids; do + if is_enabled_uid "$id"; then + echo "!$id!" >> "$tmp" + else + dbg "清理已删除设备规则: $id" + run_devgate_change del "$id" + forget_managed_uid "$id" + info "移除已删除规则: $id" + fi + done + + mv "$tmp" "$IDLIST" +} + +wait_next_check() { + local seconds="${1:-10}" + + while [ "$seconds" -gt 0 ]; do + [ -f "$RELOAD_FLAG" ] && return 0 + sleep 1 + seconds=$((seconds - 1)) + done +} + +# 主处理循环 +main_loop() { + info "设备门禁守护进程启动" + run_devgate_change start + + while :; do + local force_refresh=0 + + dbg "开始检查设备" + if [ -f "$RELOAD_FLAG" ]; then + force_refresh=1 + rm -f "$RELOAD_FLAG" 2>/dev/null + dbg "检测到配置重载标记" + fi + + if [ "$(get_enabled_device_count)" -eq 0 ]; then + if [ "$NO_ENABLED_CLEARED" -eq 0 ]; then + run_devgate_change stop + flush_firewall_changes + NO_ENABLED_CLEARED=1 + fi + wait_next_check 10 + continue + fi + + NO_ENABLED_CLEARED=0 + sync_removed_devices + for id in $(get_enabled_devices); do + sync_device_status "$id" "$force_refresh" + done + flush_firewall_changes + + wait_next_check 10 + done +} + +# 启动 +main_loop diff --git a/luci-app-devgate/root/usr/share/luci/menu.d/luci-app-devgate.json b/luci-app-devgate/root/usr/share/luci/menu.d/luci-app-devgate.json new file mode 100644 index 000000000..606031408 --- /dev/null +++ b/luci-app-devgate/root/usr/share/luci/menu.d/luci-app-devgate.json @@ -0,0 +1,24 @@ +{ + "admin/services/devgate": { + "title": "设备门禁", + "order": 90, + "action": { + "type": "view", + "path": "devgate/overview" + }, + "depends": { + "acl": ["luci-app-devgate"] + } + }, + "admin/services/devgate/remote_addr": { + "firstchild_ineligible": true, + "action": { + "type": "function", + "module": "luci.controller.devgate", + "function": "remote_addr" + }, + "depends": { + "acl": ["luci-app-devgate"] + } + } +} diff --git a/luci-app-devgate/root/usr/share/rpcd/acl.d/luci-app-devgate.json b/luci-app-devgate/root/usr/share/rpcd/acl.d/luci-app-devgate.json new file mode 100644 index 000000000..5c35309b1 --- /dev/null +++ b/luci-app-devgate/root/usr/share/rpcd/acl.d/luci-app-devgate.json @@ -0,0 +1,21 @@ +{ + "luci-app-devgate": { + "description": "授予 luci-app-devgate 设备门禁权限", + "read": { + "file": { + "/usr/lib/opkg/status": [ + "read" + ] + }, + "uci": [ + "devgate", + "network" + ] + }, + "write": { + "uci": [ + "devgate" + ] + } + } +} diff --git a/luci-app-devgate/root/usr/share/ucode/luci/controller/devgate.uc b/luci-app-devgate/root/usr/share/ucode/luci/controller/devgate.uc new file mode 100644 index 000000000..caf0d9129 --- /dev/null +++ b/luci-app-devgate/root/usr/share/ucode/luci/controller/devgate.uc @@ -0,0 +1,44 @@ +'use strict'; + +function get_request_env(http, name) { + let value = ''; + + if (http) + value = http.getenv(name) || ''; + + if (!value) + value = getenv(name) || ''; + + return value; +} + +function append_addr(list, value) { + if (value) + push(list, value); +} + +function remote_addr(env) { + let http = env && env.http; + let addr = ''; + let values = []; + let forwarded_for = get_request_env(http, 'HTTP_X_FORWARDED_FOR'); + + if (forwarded_for) { + append_addr(values, forwarded_for); + append_addr(values, get_request_env(http, 'HTTP_X_REAL_IP')); + append_addr(values, get_request_env(http, 'HTTP_CF_CONNECTING_IP')); + append_addr(values, get_request_env(http, 'HTTP_TRUE_CLIENT_IP')); + append_addr(values, get_request_env(http, 'HTTP_FORWARDED')); + } else { + append_addr(values, get_request_env(http, 'REMOTE_ADDR')); + } + + for (let value in values) + addr += value + '\n'; + + print(addr); +} + +return { + remote_addr +}; diff --git a/luci-app-diskbox/LICENSE b/luci-app-diskbox/LICENSE new file mode 100755 index 000000000..2be75b17c --- /dev/null +++ b/luci-app-diskbox/LICENSE @@ -0,0 +1,663 @@ +Copyright (C) 2026 permails + + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + LuCI DiskBox (luci-app-diskbox) - Modern Disk & Partition Manager for OpenWrt LuCI. + Copyright (C) 2026 permails + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/luci-app-diskbox/Makefile b/luci-app-diskbox/Makefile new file mode 100644 index 000000000..5a0a30442 --- /dev/null +++ b/luci-app-diskbox/Makefile @@ -0,0 +1,57 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-diskbox +LUCI_NAME:=luci-app-diskbox +PKG_VERSION:=1.26.9 +PKG_RELEASE:=1 +PKG_MAINTAINER:=permails +PKG_LICENSE:=AGPL-3.0 + +LUCI_TITLE:=DiskBox - Modern Disk Manager interface for LuCI +LUCI_DEPENDS:=+e2fsprogs +parted +smartmontools +blkid \ + +kmod-fs-vfat +dosfstools +kmod-fs-msdos +kmod-nls-base +kmod-nls-utf8 +kmod-nls-cp932 +kmod-nls-cp936 +kmod-nls-cp950 \ + +kmod-fs-exfat +exfat-mkfs +exfat-fsck \ + +kmod-fs-ntfs3 \ + +kmod-fs-btrfs \ + +PACKAGE_$(PKG_NAME)_INCLUDE_ntfs_3g_utils:ntfs-3g-utils \ + +PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs:btrfs-progs \ + +PACKAGE_$(PKG_NAME)_INCLUDE_lsblk:lsblk \ + +PACKAGE_$(PKG_NAME)_INCLUDE_mdadm:mdadm \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456:mdadm \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456:kmod-md-raid456 \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears:mdadm \ + +PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears:kmod-md-linear \ + +luci-base + +LUCI_DESCRIPTION:=Modern Disk and Partition Manager interface for OpenWrt LuCI (JavaScript client-rendered, no luci-compat required) + +define Package/$(PKG_NAME)/config +config PACKAGE_$(PKG_NAME)_INCLUDE_ntfs_3g_utils + depends on PACKAGE_$(PKG_NAME) + bool "Include ntfs-3g-utils" + default y +config PACKAGE_$(PKG_NAME)_INCLUDE_btrfs_progs + depends on PACKAGE_$(PKG_NAME) + bool "Include btrfs-progs" + default y +config PACKAGE_$(PKG_NAME)_INCLUDE_lsblk + depends on PACKAGE_$(PKG_NAME) + bool "Include lsblk" + default y +config PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + depends on PACKAGE_$(PKG_NAME) + bool "Include mdadm" + default n +config PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_raid456 + depends on PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + bool "Include kmod-md-raid456" + default n +config PACKAGE_$(PKG_NAME)_INCLUDE_kmod_md_linears + depends on PACKAGE_$(PKG_NAME)_INCLUDE_mdadm + bool "Include kmod-md-linear" + default n +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-diskbox/README.md b/luci-app-diskbox/README.md new file mode 100644 index 000000000..4e0536eba --- /dev/null +++ b/luci-app-diskbox/README.md @@ -0,0 +1,108 @@ +# LuCI DiskBox (`luci-app-diskbox`) + +**A Modern Disk, Partition, RAID & Btrfs Manager for OpenWrt LuCI** + +`luci-app-diskbox` is a standalone, lightweight, and modern disk management application designed for OpenWrt 21.02 / 23.05 / 24.10 / Master. Built with pure JavaScript client-side rendering and a dedicated RPCD backend, it operates smoothly without requiring `luci-compat`. + +--- + +## Features + +### 1. Storage Device Overview & S.M.A.R.T. Health +- **Multi-Device Support**: Full recognition of SCSI/SATA (`/dev/sd*`), NVMe (`/dev/nvme*`), MMC/eMMC (`/dev/mmcblk*`), and VirtIO (`/dev/vd*`) block storage devices. +- **Health & Temperature Monitoring**: Real-time S.M.A.R.T attribute scanning with critical bad-sector (05/C5) warnings and eMMC life-cycle/wear-level detection. +- **Visual Partition Usage**: Color-coded proportional capacity bars representing partition layouts. +- **Safe Ejection**: One-click software detachment (`sync` & SCSI device deletion) before physical unplugging to prevent kernel panics. + +### 2. Partition Management +- **Partition Table Operations**: Initialize and switch between GPT and MBR partition tables. +- **Partition Creation**: Support for optimal 2048-sector alignment and human-readable capacity input in End Sector (`+500M`, `+10G`, `+1T`). +- **Safety Protection**: Accurate detection of MBR extended partitions and prevention of accidental deletion or formatting while logical partitions or mount points are active. +- **Multi-Filesystem Formatting**: Format partitions to `ext2`, `ext3`, `ext4`, `fat32`, `exfat`, `ntfs`, `btrfs`, or `swap`. + +### 3. Software RAID (mdadm) +- **Array Creation**: Support for Linear, RAID 0, RAID 1, RAID 5, RAID 6, and RAID 10. +- **Dynamic Selection**: Standard LuCI dropdown menus to select member devices and partitions. +- **Auto Configuration**: Automatically generates UCI configurations and loads arrays upon system boot. + +### 4. Btrfs Multi-Device & Snapshot Management +- **Filesystem Creation**: Support for multi-device Btrfs pools (`single`, `raid0`, `raid1`, `raid10`, `dup`). +- **Subvolumes**: Create, delete, and set default subvolumes. +- **Snapshots**: Fast creation of read-only or read-write snapshots with custom source and destination paths. + +### 5. Mount Points +- **Active Mount Inspection**: Displays real-time mount paths and active mount options. +- **Quick Mount & Umount**: Easily mount unused partitions or unmount active mount points. + +### 6. Internationalization (i18n) +- Seamless dynamic language switching following OpenWrt system language (English, Simplified Chinese, Traditional Chinese, Polish). + +--- + +## Directory Structure + +```text +luci-app-diskbox/ +├── Makefile # OpenWrt package Makefile +├── htdocs/ # Pure JavaScript frontend views +│ └── luci-static/resources/view/diskbox/ +│ ├── disks.js # Overview, RAID, Btrfs & Mount points +│ ├── partition.js # Partition management & formatting +│ └── btrfs.js # Btrfs detail & snapshot view +├── root/ # RPCD backend and LuCI menu declarations +│ ├── etc/uci-defaults/99-luci-app-diskbox +│ ├── usr/libexec/rpcd/luci.diskbox +│ └── usr/share/ +│ ├── luci/menu.d/luci-app-diskbox.json +│ └── rpcd/acl.d/luci-app-diskbox.json +├── po/ # i18n translation catalogs +│ ├── templates/diskbox.pot +│ ├── zh-cn/diskbox.po +│ ├── zh-tw/diskbox.po +│ └── pl/diskbox.po +├── LICENSE +└── README.md +``` + +--- + +## Build from Source + +Clone the repository directly into your OpenWrt buildroot's `package` directory: + +```bash +# Navigate to your OpenWrt source root +cd openwrt + +# Clone luci-app-diskbox +git clone https://github.com/permails/luci-app-diskbox.git package/luci-app-diskbox + +# Update feeds and select in menuconfig +./scripts/feeds update -a +./scripts/feeds install -a +make menuconfig +``` + +Select the package in `menuconfig`: +```text +LuCI ---> + 3. Applications ---> + <*> luci-app-diskbox................... Modern Disk Manager interface for LuCI +``` + +Compile the package: +```bash +make package/luci-app-diskbox/compile V=s +``` + +--- + +## Author + +- **permails** ([https://github.com/permails/luci-app-diskbox](https://github.com/permails/luci-app-diskbox)) + +--- + +## License + +Licensed under the [AGPL-3.0 License](LICENSE). diff --git a/luci-app-diskbox/htdocs/luci-static/resources/view/diskbox/diskbox.css b/luci-app-diskbox/htdocs/luci-static/resources/view/diskbox/diskbox.css new file mode 100644 index 000000000..197ba33cb --- /dev/null +++ b/luci-app-diskbox/htdocs/luci-static/resources/view/diskbox/diskbox.css @@ -0,0 +1,702 @@ +/** + * luci-app-diskbox — Modern Storage Manager component styles + * Ported from the DESIGN_SPEC demo. Built purely on top of OpenWrt 25 + * cascade.css (uses only tokens cascade.css already defines, so light/dark + * themes both adapt). Does NOT override standard LuCI buttons, form + * controls, or the CBI value layout — only adds the partition-map / card + * / table-typography components. + */ + +/* Visual Partition Map Component */ +/* Option B: Modern Storage Manager Partition Cards (群晖 DSM / TrueNAS 风格) */ +.disk-partition-card { + background: var(--background-color-high); + border: 1px solid var(--border-color-low); + border-radius: 6px; + padding: 12px 16px; + margin-bottom: 14px; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.disk-partition-card:hover { + border-color: var(--border-color-medium); + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04); +} + +.disk-part-card-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 6px; + font-size: 12px; +} + +.disk-part-card-title { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; +} + +.disk-bus-badge { + font-size: 10px; + font-weight: 600; + padding: 1px 6px; + border-radius: 3px; + background: var(--background-color-medium); + border: 1px solid var(--border-color-high); + color: var(--text-color-high); + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.disk-part-path { + font-size: 13px; + color: var(--text-color-highest); +} + +.disk-part-model { + color: var(--text-color-medium); + font-size: 12px; +} + +.disk-part-spec { + color: var(--text-color-low); + font-size: 11px; +} + +.disk-part-card-meta { + display: flex; + align-items: center; + gap: 12px; + font-size: 12px; + color: var(--text-color-medium); +} + +.disk-meta-tag { + display: inline-flex; + align-items: center; + gap: 4px; +} + +/* Slender, refined 14px pill partition bar */ +.disk-partition-bar { + width: 100%; + height: 14px; + background-color: var(--background-color-medium); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 7px; + overflow: hidden; + display: flex; + position: relative; + box-sizing: border-box; + margin: 8px 0 10px 0; +} + +.part-segment { + height: 100%; + min-width: 90px; + display: flex; + align-items: center; + justify-content: center; + padding: 0 4px; + color: #ffffff; + font-size: 10px; + font-weight: 500; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + cursor: pointer; + position: relative; + border-right: 1px solid rgba(255, 255, 255, 0.4); + transition: filter 0.15s ease, opacity 0.15s ease; + user-select: none; + box-sizing: border-box; +} + +.part-segment:last-child { + border-right: none; +} + +.part-segment:hover { + filter: brightness(1.15); +} + +/* Five-state semantic partition palette (DESIGN_SPEC) — fixed hues so the + state reads the same in light and dark. */ +.part-state-boot { + background-color: #546e7a !important; /* Muted Slate - System/Boot */ + color: #ffffff !important; +} + +.part-state-inuse { + background-color: #1e88e5 !important; /* Clean Modern Blue - Mounted */ + color: #ffffff !important; +} + +.part-state-unmounted { + background-color: #f59e0b !important; /* Soft Amber - Unmounted */ + color: #ffffff !important; +} + +.part-state-raw { + background-color: #94a3b8 !important; /* Neutral Cool Gray - Empty/Raw */ + color: #ffffff !important; +} + +.part-state-unalloc { + background-color: #f1f5f9 !important; + color: #64748b !important; + background-image: repeating-linear-gradient( + 45deg, + rgba(0, 0, 0, 0.02), + rgba(0, 0, 0, 0.02) 6px, + rgba(0, 0, 0, 0.05) 6px, + rgba(0, 0, 0, 0.05) 12px + ) !important; + border-right: none; +} + +.part-state-unalloc:hover { + background-color: #e2e8f0 !important; +} + +/* Refined Partition Legend */ +.disk-partition-legend { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 12px; + font-size: 12px; + color: var(--text-color-medium); + padding: 7px 14px; + background: var(--background-color-medium); + border-radius: 4px; + border: 1px solid var(--border-color-low); +} + +.legend-item { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.legend-color-dot { + width: 9px; + height: 9px; + border-radius: 2px; + display: inline-block; +} + +/* Segment Tooltip */ +.part-tooltip { + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%); + background: var(--text-color-highest); + color: var(--background-color-high); + padding: 6px 10px; + border-radius: 3px; + font-size: 11px; + font-weight: normal; + white-space: nowrap; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); + z-index: 50; + pointer-events: none; + display: none; + line-height: 1.4; + text-align: left; +} + +.part-tooltip::after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border-width: 4px; + border-style: solid; + border-color: var(--text-color-highest) transparent transparent transparent; +} + +.part-segment:hover .part-tooltip { + display: block; +} + +/* Health and SMART Link */ +.smart-status-link { + font-weight: bold; + cursor: pointer; + text-decoration: underline; +} +.smart-status-link.passed { + color: var(--success-color-high); +} +.smart-status-link.warning { + color: var(--warn-color-high); +} +.smart-status-link.failed { + color: var(--error-color-high); +} + +/* Hardware Info Two-Column Grid */ +.hw-info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 8px 24px; + padding: 4px 0; +} + +.hw-info-entry { + padding: 6px 0; + border-bottom: 1px solid var(--border-color-low); +} + +.hw-info-label { + font-size: 11px; + font-weight: bold; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--text-color-medium); + margin-bottom: 2px; +} + +.hw-info-value { + font-size: 13px; + color: var(--text-color-highest); + word-break: break-all; +} + +/* Command preview in modal */ +.command-preview-box { + background: var(--background-color-medium); + color: var(--primary-color-low); + font-family: monospace; + font-size: 12px; + padding: 8px 12px; + border-radius: 3px; + border: 1px solid var(--border-color-high); + margin: 8px 0; + overflow-x: auto; +} + + +/* OpenWrt Standard Tabs (选项卡标签) - NOT bold, NOT enlarged, 100% Native Font */ +.cbi-tabmenu, +.cbi-tabmenu.map, +.tabs { + margin: 0 0 16px 0 !important; + padding: 0 2px !important; + font-family: var(--font-sans) !important; +} + +.cbi-tabmenu > li, +.cbi-tabmenu.map > li, +.tabs > li { + font-family: var(--font-sans) !important; + font-size: 13px !important; + font-weight: normal !important; + height: 25px !important; + line-height: 25px !important; +} + +.cbi-tabmenu > li > a, +.cbi-tabmenu.map > li > a, +.tabs > li > a { + font-family: var(--font-sans) !important; + font-size: 13px !important; + font-weight: normal !important; + line-height: 18px !important; + padding: 4px 8px !important; +} + +/* Table headers strictly left-aligned (matching body cells), OpenWrt native font, NOT bold, NOT enlarged */ +.th.cbi-section-actions, +.table.cbi-section-table .th, +.disk-card-table .th, +.partition-master-table .th { + font-family: var(--font-sans) !important; + font-size: 13px !important; + font-weight: normal !important; + background-color: var(--background-color-medium) !important; + border-bottom: 2px solid var(--border-color-high) !important; + text-align: left !important; + white-space: nowrap !important; + vertical-align: middle !important; + line-height: 18px !important; +} + +/* Table body cell contents strictly left-aligned, OpenWrt native font, NOT bold, NOT enlarged */ +.td.cbi-section-actions, +.table.cbi-section-table .td, +.disk-card-table .td, +.partition-master-table .td { + font-family: var(--font-sans) !important; + font-size: 13px !important; + font-weight: normal !important; + vertical-align: middle !important; + padding: 8px 8px !important; + box-sizing: border-box !important; + overflow: hidden !important; + line-height: 18px !important; + text-align: left !important; +} + +/* Overview table (physical disks): keep every cell on a single line. The + wrapping container carries overflow-x:auto, so it scrolls rather than wraps. */ +.disk-overview-table .td { + white-space: nowrap !important; +} + +/* Actions column of the Physical Disks table. The guaranteed width comes from + the fixed-width (180px) emitted in disks.js — a %-width column plus the + table's min-width lets Chromium keep the column narrower than the cell's + min-width, which clips the "Manage Partitions" button. This min-width is a + harmless fallback in case that col is ever reverted to a percentage. */ +.disk-overview-table .td.cbi-section-actions { + min-width: 175px; +} + +/* Ensure no artificial bolding in tables */ +.table strong, +.disk-card-table strong, +.partition-master-table strong, +.cbi-section-table strong { + font-weight: normal !important; +} + +/* Universal OpenWrt Native Font for ALL tables and ALL table contents */ +.table, +.table *, +.disk-card-table, +.disk-card-table *, +.partition-master-table, +.partition-master-table *, +.cbi-section-table, +.cbi-section-table * { + font-family: var(--font-sans) !important; +} + +.table code, +.disk-card-table code, +.partition-master-table code, +.cbi-section-table code { + font-family: var(--font-sans) !important; + background: transparent !important; + border: none !important; + padding: 0 !important; + color: inherit !important; + font-size: 13px !important; + font-weight: normal !important; +} + +/* Inline table mount dropdown & input controls */ +.table select.cbi-input-select, +.disk-card-table select.cbi-input-select { + font-family: var(--font-sans) !important; + font-size: 12px !important; + font-weight: normal !important; + height: 26px !important; + padding: 2px 6px !important; + box-sizing: border-box !important; + vertical-align: middle !important; +} + +.table input.cbi-input-text, +.disk-card-table input.cbi-input-text { + font-family: var(--font-sans) !important; + font-size: 12px !important; + font-weight: normal !important; + height: 26px !important; + padding: 2px 6px !important; + box-sizing: border-box !important; + vertical-align: middle !important; +} + +/* Action buttons strictly in a single horizontal row, left-aligned */ +.td.cbi-section-actions .btn-group-actions, +.td.cbi-section-actions > div { + display: inline-flex !important; + flex-direction: row !important; + flex-wrap: nowrap !important; + gap: 6px !important; + justify-content: flex-start !important; + align-items: center !important; + white-space: nowrap !important; +} + +.td.cbi-section-actions .btn { + white-space: nowrap !important; + display: inline-block !important; + margin: 0 !important; + flex: 0 0 auto !important; +} + +/* ========================================================================== + Unified Master Partition Table & Strict Column Alignment + ========================================================================== */ +.disk-card-table, +.partition-master-table { + table-layout: fixed !important; + width: 100% !important; + border-collapse: collapse !important; + margin-bottom: 0 !important; +} + +.disk-card-table .col-device, +.disk-card-table .col-fs, +.disk-card-table .col-mount, +.disk-card-table .col-size, +.disk-card-table .col-automount, +.disk-card-table .col-actions { + white-space: nowrap !important; +} + +.disk-card-table .col-mount { + overflow: hidden !important; + text-overflow: ellipsis !important; +} + +.disk-card-table .cbi-progressbar { + min-width: 0 !important; + width: 80px !important; + max-width: 80px !important; + margin: 1.3em 0 2px 0 !important; +} + +.disk-card-table .cbi-progressbar::before { + font-size: 11px !important; + line-height: 14px !important; + top: -1.3em !important; + left: 0 !important; + right: auto !important; + width: auto !important; + text-align: left !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} + +/* Form fields left-aligned in Create RAID and Create Btrfs Pool */ +.cbi-value { + text-align: left !important; +} + +.cbi-value label.cbi-value-title, +.cbi-value .cbi-value-title { + text-align: left !important; + width: 140px !important; + min-width: 140px !important; + flex: 0 0 140px !important; +} + +.cbi-value .cbi-value-field { + text-align: left !important; + align-items: flex-start !important; + justify-content: flex-start !important; +} + +.cbi-value .cbi-value-description { + text-align: left !important; +} + +.cbi-page-actions { + display: flex; + justify-content: flex-start !important; + gap: 10px; + background: var(--background-color-high) !important; + border-top: none !important; +} + +/* Status Badges */ +.badge-status { + display: inline-block; + padding: 2px 6px; + font-size: 11px; + border-radius: 3px; + font-weight: bold; + margin-right: 6px; + vertical-align: middle; + line-height: 14px; +} + +.badge-mounted { + background: rgba(46, 125, 50, 0.12); + color: var(--success-color-high); + border: 1px solid rgba(46, 125, 50, 0.3); +} + +.badge-unmounted { + background: var(--background-color-medium); + color: var(--text-color-medium); + border: 1px solid var(--border-color-high); +} + +.mount-options-text { + font-family: monospace; + font-size: 11px; + color: var(--text-color-medium); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + max-width: 100%; +} + +/* Disk card section (per-physical-disk block on the Partition tab). It is a + fieldset so the title anchors at the top-left; the UA fieldset + border/padding is reset so it reads as a clean section, not a form frame. */ +.disk-card-section { + margin: 0 0 16px 0; + padding: 14px 16px; + border: 1px solid var(--border-color-low); + border-radius: 4px; + background: var(--background-color-high); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); + min-width: 0; +} + +.disk-card-section > legend { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; + font-size: 13px; + font-weight: 600; + color: var(--text-color-highest); +} + +.disk-card-bar-wrap { + margin-bottom: 12px; + padding: 8px 12px; + background: var(--background-color-medium); + border: 1px solid var(--border-color-low); + border-radius: 4px; +} + +/* Card-scoped action toolbar (bottom of a per-disk card) */ +.disk-card-actions-bar { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; + padding: 8px 14px; + background: var(--background-color-medium); + border-top: 1px solid var(--border-color-low); +} + +.disk-actions-flex { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + gap: 8px; + flex-wrap: wrap; +} + +.disk-actions-left, +.disk-actions-right { + display: flex; + gap: 8px; + align-items: center; +} + +/* ========================================================================== + RAID & Btrfs Enhanced Components + ========================================================================== */ +.badge-tech { + font-family: monospace; + font-size: 11px; + padding: 2px 6px; + border-radius: 3px; + background: var(--background-color-medium); + color: var(--text-color-high); + border: 1px solid var(--border-color-high); + display: inline-block; + vertical-align: middle; +} + +.badge-member { + font-family: monospace; + font-size: 11px; + padding: 2px 6px; + border-radius: 3px; + background: rgba(33, 150, 243, 0.08); + color: var(--primary-color-high); + border: 1px solid rgba(33, 150, 243, 0.3); + display: inline-block; + margin: 2px 4px 2px 0; + white-space: nowrap; +} + +.btrfs-pool-card, +.raid-pool-card { + margin-bottom: 24px; +} + +.pool-meta-bar { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; + padding: 8px 12px; + margin-bottom: 12px; + background: var(--background-color-medium); + border: 1px solid var(--border-color-low); + border-radius: 4px; + font-size: 12px; +} + +.pool-meta-item { + display: flex; + align-items: center; + gap: 6px; +} + +.checkbox-chip-group { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 4px; +} + +.checkbox-chip { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 5px 10px; + background: transparent; + border: 1px solid var(--border-color-high); + border-radius: 3px; + cursor: pointer; + font-size: 12px; + user-select: none; + transition: background 0.15s ease, border-color 0.15s ease; +} + +.checkbox-chip:hover { + background: var(--background-color-low); +} + +.checkbox-chip input[type="checkbox"] { + margin: 0; + cursor: pointer; +} + +/* Empty-state placeholder for the member selector (no free partitions). */ +.chip-empty-box { + border: 1px dashed var(--border-color-medium); + border-radius: 4px; + padding: 10px 14px; + background: var(--background-color-medium); + color: var(--text-color-medium); + font-size: 12px; + font-style: italic; +} diff --git a/luci-app-diskbox/htdocs/luci-static/resources/view/diskbox/disks.js b/luci-app-diskbox/htdocs/luci-static/resources/view/diskbox/disks.js new file mode 100644 index 000000000..9285b166e --- /dev/null +++ b/luci-app-diskbox/htdocs/luci-static/resources/view/diskbox/disks.js @@ -0,0 +1,1771 @@ +'use strict'; +// Copyright (C) 2026 permails +// Licensed under the GNU General Public License v3.0 +// +// Modern Storage Manager (4-tab single page): 物理磁盘 / 分区管理 / RAID / Btrfs. +// Ported to the LuCI AMD view architecture from the DESIGN_SPEC demo. Every +// action is wired to the real `luci.diskbox` rpcd backend; no mock data. All +// protection rules are preserved: is_boot partitions are locked, / , /rom and +// /overlay* are brick-level (never unmountable), mount targets require +// >= 64 MiB, and the backend's destructive `mk_partition_table` (parted +// mklabel) is only offered on disks that have no partitions yet. +'require view'; +'require rpc'; +'require ui'; +'require dom'; + +var callListDevices = rpc.declare({ + object: 'luci.diskbox', + method: 'list_devices', + expect: { devices: {} } +}); + +var callGetDiskInfo = rpc.declare({ + object: 'luci.diskbox', + method: 'get_disk_info', + params: ['device'] +}); + +var callGetSmartAttr = rpc.declare({ + object: 'luci.diskbox', + method: 'get_smart_attr', + params: ['device'] +}); + +var callGetFormatCmd = rpc.declare({ + object: 'luci.diskbox', + method: 'get_format_cmd', + expect: { formats: {} } +}); + +var callFormatPartition = rpc.declare({ + object: 'luci.diskbox', + method: 'format_partition', + params: ['partition', 'filesystem', 'force'] +}); + +var callMkPartitionTable = rpc.declare({ + object: 'luci.diskbox', + method: 'mk_partition_table', + params: ['device', 'table'] +}); + +var callCreatePartition = rpc.declare({ + object: 'luci.diskbox', + method: 'create_partition', + params: ['device', 'start_sec', 'end_sec', 'type'] +}); + +var callRemovePartition = rpc.declare({ + object: 'luci.diskbox', + method: 'remove_partition', + params: ['device', 'number'] +}); + +var callEjectDevice = rpc.declare({ + object: 'luci.diskbox', + method: 'eject_device', + params: ['device'] +}); + +var callRescanDisks = rpc.declare({ + object: 'luci.diskbox', + method: 'rescan_disks' +}); + +var callGetMountPoints = rpc.declare({ + object: 'luci.diskbox', + method: 'get_mount_points', + expect: { mount_points: [] } +}); + +var callMount = rpc.declare({ + object: 'luci.diskbox', + method: 'mount', + params: ['device', 'point', 'fs', 'options'] +}); + +var callUmount = rpc.declare({ + object: 'luci.diskbox', + method: 'umount', + params: ['point'] +}); + +var callListRaidDevices = rpc.declare({ + object: 'luci.diskbox', + method: 'list_raid_devices', + expect: { raid_devices: {} } +}); + +var callCreateRaid = rpc.declare({ + object: 'luci.diskbox', + method: 'create_raid', + params: ['name', 'level', 'members'] +}); + +var callListBtrfsDevices = rpc.declare({ + object: 'luci.diskbox', + method: 'list_btrfs_devices', + expect: { btrfs_devices: {} } +}); + +var callGetBtrfsInfo = rpc.declare({ + object: 'luci.diskbox', + method: 'get_btrfs_info', + params: ['uuid'] +}); + +var callGetBtrfsSubvolumes = rpc.declare({ + object: 'luci.diskbox', + method: 'get_btrfs_subvolumes', + params: ['uuid'], + expect: { subvolumes: [] } +}); + +var callCreateBtrfs = rpc.declare({ + object: 'luci.diskbox', + method: 'create_btrfs', + params: ['label', 'level', 'members'] +}); + +var callBtrfsSetLabel = rpc.declare({ + object: 'luci.diskbox', + method: 'btrfs_set_label', + params: ['uuid', 'label'] +}); + +var callBtrfsSubvolCreate = rpc.declare({ + object: 'luci.diskbox', + method: 'btrfs_subvol_create', + params: ['uuid', 'path'] +}); + +var callBtrfsSubvolDelete = rpc.declare({ + object: 'luci.diskbox', + method: 'btrfs_subvol_delete', + params: ['uuid', 'path'] +}); + +var callBtrfsSubvolSetDefault = rpc.declare({ + object: 'luci.diskbox', + method: 'btrfs_subvol_set_default', + params: ['uuid', 'path'] +}); + +var callBtrfsSnapshotCreate = rpc.declare({ + object: 'luci.diskbox', + method: 'btrfs_snapshot_create', + params: ['uuid', 'source', 'dest', 'readonly'] +}); + +// Minimum size for a meaningful mount target (bytes). System/boot partitions +// (is_boot) and tiny firmware slivers (< 64 MiB) are excluded from the mount +// picker so they can never be chosen as a mount point. +var MIN_MOUNT_BYTES = 64 * 1024 * 1024; + +// Cache-busting token for the component stylesheet (bump on each CSS change). +var DISKBOX_CSS_V = '20260919_a'; + +// Active top tab (module-level so it survives re-renders within a page load). +// Persisted in the URL hash (#partitions) so a location.reload() fired after a +// mount / delete / eject / rescan returns the user to the SAME tab instead of +// bouncing back to the default Physical Disks tab and hiding the result. +var MAIN_TAB_KEYS = ['disks', 'partitions', 'raid', 'btrfs']; +var activeMainTab = (function() { + try { + var h = (typeof location !== 'undefined' && location.hash) + ? location.hash.replace(/^#/, '') : ''; + if (MAIN_TAB_KEYS.indexOf(h) !== -1) return h; + } catch (e) { /* ignore */ } + return 'disks'; +})(); +// Reflect the active tab in the URL hash. replaceState (not location.hash=) +// keeps the back button clean - no new history entry per tab click. +function syncTabHash() { + try { + if (typeof history !== 'undefined' && history.replaceState) + history.replaceState(null, '', '#' + activeMainTab); + } catch (e) { /* some embedded contexts block replaceState */ } +} +// Set by render(); lets buttons deep in a tab switch to another tab + redraw. +var currentDraw = null; +// Per-disk get_disk_info cache (this page's JS context). Re-selecting the +// Partition Management tab then renders instantly instead of re-running the +// slow smartctl-based backend query. Every mutating action ends in +// location.reload(), which rebuilds the context and resets this cache. +var _diskInfoCache = {}; + +// Inject the component stylesheet once. There is no LuCI template for this +// view, so we add a to document.head from the render step. +function ensureStyles() { + if (typeof document === 'undefined' || document.getElementById('diskbox-static-css')) return; + var link = document.createElement('link'); + link.id = 'diskbox-static-css'; + link.rel = 'stylesheet'; + link.href = '/luci-static/resources/view/diskbox/diskbox.css?v=' + DISKBOX_CSS_V; + (document.head || document.getElementsByTagName('head')[0]).appendChild(link); +} + +// Switch to another top tab, redraw, and (optionally) scroll to a per-disk +// section by id `disk-group-`. +function gotoTab(key, devName) { + activeMainTab = key; + syncTabHash(); + if (typeof currentDraw === 'function') currentDraw(); + if (devName) { + setTimeout(function() { + var el = document.getElementById('disk-group-' + devName); + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }); + }, 80); + } +} + +function translatePowerStatus(status) { + if (!status || status === '-') return '-'; + var map = { + 'ACTIVE': _('Active'), + 'STANDBY': _('Standby'), + 'SLEEP': _('Sleep') + }; + return map[status] || status; +} + +function translateHealth(health) { + if (!health || health === '-') return '-'; + var map = { + 'Normal': _('Normal'), + 'PASSED': _('Passed'), + 'Warning': _('Warning'), + 'Urgent': _('Urgent'), + 'FAILED': _('Failed') + }; + return map[health] || health; +} + +function translateFs(fs, type) { + if (type === 'extended' || fs === 'extended') return _('Extended Partition'); + if (fs === 'Free Space' || type === 'free') return _('Free Space'); + if (fs === 'raw') return _('Unformatted'); + if (!fs || fs === '-') return '-'; + return fs.toUpperCase(); +} + +// Human-readable size from a byte count (1024-based), e.g. 113.17 GB. +function formatBytes(bytes) { + if (!bytes || bytes <= 0) return '0 B'; + var units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; + var b = bytes, i = 0; + while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } + return (i === 0 ? b.toFixed(0) : b.toFixed(2)) + ' ' + units[i]; +} + +// Five-state semantic classification purely from backend facts (no size +// guessing): is_boot -> protected; mounted -> in use; unmounted + fstype -> +// data present; unmounted + no fs -> empty/safe. Unallocated is computed +// separately by the bar/chip builders. +function partState(p) { + var fstype = (p.fstype || p.fs) || ''; + var mounted = p.mount_point && p.mount_point !== '-'; + if (p.is_boot) return { cls: 'part-state-boot', tag: _('Boot / Firmware (protected)') }; + if (mounted) return { cls: 'part-state-inuse', tag: _('Data (in use)') }; + var hasFs = fstype && fstype !== 'raw' && fstype !== '-' && fstype !== 'unknown' && + fstype !== 'Free Space' && fstype.toLowerCase() !== 'free space'; + if (hasFs) return { cls: 'part-state-unmounted', tag: _('Data (unmounted)') }; + return { cls: 'part-state-raw', tag: _('Empty (no filesystem)') }; +} + +// A label+checkbox "member" chip for the RAID / Btrfs creation forms. +function memberChip(value, label) { + var cb = E('input', { 'type': 'checkbox', 'value': value }); + return E('label', { 'class': 'checkbox-chip' }, [ cb, E('span', {}, label) ]); +} + +// Whether a partition / whole-disk entry is already mounted. Mounted devices +// must never be RAID/Btrfs members - creating the array/filesystem would +// ERASE them. +function isMountedPart(p) { + return p.inuse === true || (p.mount_point && p.mount_point !== '-'); +} + +// Build the group of selectable member chips from the device list. Eligible +// members = free, unmounted, non-boot partitions (or a whole disk with no +// partitions). System disks, is_boot partitions and anything already mounted +// are excluded - they are never safe RAID/Btrfs members. When no member +// qualifies, emptyMsg (if given) is shown in place of the chips so the field +// reads as "nothing to pick" rather than a missing control. +function buildMemberChips(devices, emptyMsg) { + var group = E('div', { 'class': 'checkbox-chip-group' }); + Object.keys(devices).forEach(function(k) { + var dev = devices[k]; + if (!dev) return; + var parts = dev.partitions || []; + if (parts.length === 0) { + if (!dev.is_system && !isMountedPart(dev)) { + group.appendChild(memberChip(dev.path, dev.path + ' ' + (dev.size_formated || ''))); + } + } else { + parts.forEach(function(p) { + // eMMC hardware boot partitions (mmcblkXboot0/1) hold firmware / + // bootloader data. Even when the backend's is_boot flag misses + // them, never offer them as RAID/Btrfs members. + var isBootDev = (p.name || '').indexOf('boot') !== -1; + if (!p.is_boot && !isMountedPart(p) && !isBootDev) { + group.appendChild(memberChip(p.path, p.path + ' ' + (p.size_formated || ''))); + } + }); + } + }); + if (!group.firstChild && emptyMsg) { + group.appendChild(E('div', { 'class': 'chip-empty-box' }, emptyMsg)); + } + return group; +} + +// Find a mount point for a given /dev/... device in the mount-point list. +function mountPointFor(mountPoints, devPath) { + if (!mountPoints || !devPath) return null; + for (var i = 0; i < mountPoints.length; i++) { + if (mountPoints[i] && mountPoints[i].device === devPath) return mountPoints[i].mount_point; + } + return null; +} + +// Find the mount point of any member device of a Btrfs pool (a pool is mounted +// through one of its member devices). +function btrfsMountPoint(mountPoints, membersStr) { + if (!membersStr) return null; + var members = membersStr.split(',').map(function(s) { return s.trim(); }).filter(Boolean); + for (var i = 0; i < members.length; i++) { + var mp = mountPointFor(mountPoints, members[i]); + if (mp) return mp; + } + return null; +} + +return view.extend({ + load: function() { + return Promise.all([ + callListDevices(), + callGetMountPoints(), + callListRaidDevices(), + callListBtrfsDevices(), + callGetFormatCmd() + ]); + }, + + // On-theme confirmation dialog (replaces native confirm()). Cancel is + // neutral, the destructive confirm is negative - consistent with the + // rest of the app's modals. onDismiss runs on Cancel (if given). + showConfirm: function(title, message, onConfirm, onDismiss) { + ui.showModal(title, [ + E('p', { 'style': 'white-space:pre-line; margin:0 0 1.25rem 0;' }, message), + E('div', { 'class': 'right', 'style': 'margin-top:1rem; display:flex; justify-content:flex-end; gap:8px;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'click': function(ev) { ev.preventDefault(); ui.hideModal(); if (typeof onDismiss === 'function') onDismiss(); } + }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'click': function(ev) { ev.preventDefault(); ui.hideModal(); if (typeof onConfirm === 'function') onConfirm(); } + }, _('Confirm')) + ]) + ]); + }, + + showRaidInfoModal: function(r) { + var members = (r.members_str || '').split(',').map(function(s) { return s.trim(); }).filter(Boolean); + var rows = [ + [ _('RAID Level'), r.level || '-' ], + [ _('Status'), ((r.active || '') + (r.status ? ' / ' + r.status : '')) || '-' ], + [ _('Size'), r.size_formated || '-' ], + [ _('Members'), members.join(', ') || '-' ] + ]; + var t = E('table', { 'class': 'table cbi-section-table' }, [ + rows.map(function(row) { + return E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'font-weight:600; width:40%;' }, row[0]), + E('td', { 'class': 'td' }, row[1]) + ]); + }) + ]); + ui.showModal(_('RAID Details') + ' - ' + (r.path || r.name), [ t ]); + }, + + showSmartModal: function(dev) { + ui.showModal(_('S.M.A.R.T Attributes') + ' - /dev/' + dev, [ + E('p', { 'class': 'spinning' }, _('Collecting data...')) + ]); + + callGetSmartAttr(dev).then(function(res) { + var attrs = (res && res.attributes) ? res.attributes : (Array.isArray(res) ? res : []); + if (attrs.length === 0) { + ui.showModal(_('S.M.A.R.T Attributes') + ' - /dev/' + dev, [ + E('p', { 'style': 'font-style:italic; padding:1rem;' }, _('No SMART attributes to display.')) + ]); + return; + } + + var modalContent; + if (attrs[0].key !== undefined) { + var table = E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Key')), + E('th', { 'class': 'th' }, _('Value')) + ]) + ]); + attrs.forEach(function(item) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td' }, item.key), + E('td', { 'class': 'td' }, item.value) + ])); + }); + modalContent = table; + } else { + var table = E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('ID')), + E('th', { 'class': 'th' }, _('Attribute')), + E('th', { 'class': 'th' }, _('Flag')), + E('th', { 'class': 'th' }, _('Value')), + E('th', { 'class': 'th' }, _('Worst')), + E('th', { 'class': 'th' }, _('Thresh')), + E('th', { 'class': 'th' }, _('Type')), + E('th', { 'class': 'th' }, _('Updated')), + E('th', { 'class': 'th' }, _('Raw')) + ]) + ]); + + attrs.forEach(function(item) { + var isCritical = ((item.id === '05' || item.id === 'C5') && item.raw !== '0'); + var rowStyle = isCritical ? 'background-color:rgba(var(--error-color-high-rgb), 0.25) !important;' : ''; + + table.appendChild(E('tr', { 'class': 'tr', 'style': rowStyle }, [ + E('td', { 'class': 'td' }, E('code', {}, item.id)), + E('td', { 'class': 'td' }, item.attrbute || item.attribute), + E('td', { 'class': 'td' }, item.flag || '-'), + E('td', { 'class': 'td' }, item.value || '-'), + E('td', { 'class': 'td' }, item.worst || '-'), + E('td', { 'class': 'td' }, item.thresh || '-'), + E('td', { 'class': 'td' }, item.type || '-'), + E('td', { 'class': 'td' }, item.updated || '-'), + E('td', { 'class': 'td' }, E('strong', {}, item.raw || '-')) + ])); + }); + modalContent = table; + } + + ui.showModal(_('S.M.A.R.T Attributes') + ' - /dev/' + dev, [ + E('div', { 'style': 'max-height:70vh; overflow-y:auto;' }, [ modalContent ]), + E('div', { 'class': 'right', 'style': 'margin-top:1rem;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'click': ui.hideModal + }, _('Close')) + ]) + ]); + }); + }, + + showFormatModal: function(partName, formatCmds, currentFs, onSuccess) { + var selFs = E('select', { 'class': 'cbi-input-select', 'style': 'width:100%;' }); + Object.keys(formatCmds).forEach(function(fs) { + var opt = E('option', { 'value': fs }, fs.toUpperCase()); + if (fs === currentFs) opt.selected = true; + selFs.appendChild(opt); + }); + + var statusP = E('p', { 'style': 'color:var(--error-color-high); font-weight:bold; margin-top:8px;' }); + + ui.showModal(_('Format partition: /dev/%s').format(partName), [ + E('p', {}, _('Formatting partition will ERASE all data stored on it.')), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('File System')), + E('div', { 'class': 'cbi-value-field' }, selFs) + ]), + statusP, + E('div', { 'class': 'right', 'style': 'margin-top:1.5rem; display:flex; justify-content:flex-end; gap:8px;' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-reset', + 'click': ui.hideModal + }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': function(ev) { + ev.preventDefault(); + var targetFs = selFs.value; + this.disabled = true; + callFormatPartition(partName, targetFs).then(function(res) { + if (res && res.code === 3) { + // Soft block: partition holds data - ask for confirmation then retry with force + this.disabled = false; + dom.content(statusP, [ E('p', { 'style': 'color:var(--warn-color-high); font-weight:bold;' }, (res.error || _('This partition contains data.')) + ' ') ]); + var confirmBtn = E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': function() { + this.disabled = true; + dom.content(statusP, [ E('span', { 'class': 'spinning' }, _('Formatting...')) ]); + callFormatPartition(partName, targetFs, 1).then(function(r2) { + if (r2 && r2.code === 0) { + ui.addNotification(null, E('p', {}, _('Partition formatted successfully!'))); + ui.hideModal(); + if (typeof onSuccess === 'function') { + onSuccess(); + } else { + location.reload(); + } + } else { + this.disabled = false; + dom.content(statusP, r2 ? (r2.error || _('Format failed.')) : _('Format failed.')); + } + }.bind(this)); + } + }, _('Force Format')); + var cancelBtn = E('button', { 'class': 'btn cbi-button cbi-button-reset', 'click': ui.hideModal }, _('Cancel')); + statusP.appendChild(E('div', { 'style': 'display:flex; gap:8px; margin-top:8px;' }, [confirmBtn, cancelBtn])); + } else if (res && res.code === 0) { + ui.addNotification(null, E('p', {}, _('Partition formatted successfully!'))); + ui.hideModal(); + if (typeof onSuccess === 'function') onSuccess(); + else location.reload(); + } else { + this.disabled = false; + dom.content(statusP, res ? (res.error || _('Format failed.')) : _('Format failed.')); + } + }.bind(this)); + } + }, _('Format')) + ]) + ]); + }, + + // Mount / re-mount dialog. Candidates are non-boot partitions >= 64 MiB (or + // a whole non-system disk if it has no partitions). Prefills the selected + // partition's current point + options when editing an existing mount. + showMountModal: function(devName, dev, info, selectedPart, data) { + var mountPoints = data[1] || []; + var parts = (info && info.partitions) || (dev && dev.partitions) || []; + var candidates = []; + parts.forEach(function(p) { + if (p.is_boot) return; + if ((p.size || 0) < MIN_MOUNT_BYTES) return; + if (p.number && p.number > 0) candidates.push({ path: p.path, label: p.path + ' ' + (p.size_formated || '') }); + }); + if (candidates.length === 0 && dev && !dev.is_system) { + candidates.push({ path: dev.path, label: dev.path + ' ' + (dev.size_formated || '') }); + } + + var curPoint = (selectedPart && selectedPart.mount_point && selectedPart.mount_point !== '-') + ? selectedPart.mount_point : '/mnt/'; + if (selectedPart && curPoint === '/mnt/' && selectedPart.name) curPoint = '/mnt/' + selectedPart.name; + var curOpts = 'rw,noatime'; + mountPoints.forEach(function(mp) { + if (selectedPart && mp.device === selectedPart.path && mp.mount_options) curOpts = mp.mount_options; + }); + + var devSel = E('select', { 'class': 'cbi-input-select', 'style': 'width:100%;' }); + if (candidates.length === 0) { + devSel.appendChild(E('option', { 'value': '' }, _('-- No mountable partitions --'))); + } + candidates.forEach(function(c) { + var o = E('option', { 'value': c.path }, c.label); + if (selectedPart && c.path === selectedPart.path) o.selected = true; + devSel.appendChild(o); + }); + + var fsSel = E('select', { 'class': 'cbi-input-select', 'style': 'width:100%;' }, [ + E('option', { 'value': 'auto', 'selected': 'selected' }, 'auto'), + E('option', { 'value': 'ext4' }, 'ext4'), + E('option', { 'value': 'ext3' }, 'ext3'), + E('option', { 'value': 'ext2' }, 'ext2'), + E('option', { 'value': 'vfat' }, 'vfat'), + E('option', { 'value': 'exfat' }, 'exfat'), + E('option', { 'value': 'ntfs' }, 'ntfs'), + E('option', { 'value': 'btrfs' }, 'btrfs') + ]); + var pointIn = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'value': curPoint }); + var optsIn = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'value': curOpts }); + + ui.showModal(_('Mount a device'), [ + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Device')), E('div', { 'class': 'cbi-value-field' }, devSel) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('File System')), E('div', { 'class': 'cbi-value-field' }, fsSel) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Mount Point')), E('div', { 'class': 'cbi-value-field' }, pointIn) ]), + E('div', { 'class': 'cbi-value' }, [ E('label', { 'class': 'cbi-value-title' }, _('Mount Options')), E('div', { 'class': 'cbi-value-field' }, optsIn) ]), + E('div', { 'class': 'right', 'style': 'margin-top:1.5rem; display:flex; justify-content:flex-end; gap:8px;' }, [ + E('button', { 'class': 'btn cbi-button cbi-button-reset', 'click': ui.hideModal }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button cbi-button-add', + 'click': function(ev) { + ev.preventDefault(); + var d = devSel.value, p = pointIn.value; + if (!d || !p) { + ui.addNotification(null, E('p', {}, _('Please select a device and input mount point!'))); + return; + } + this.disabled = true; + ui.showModal(_('Mounting'), [ E('p', { 'class': 'spinning' }, _('Mounting device...')) ]); + callMount(d, p, fsSel.value, optsIn.value).then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to mount.'))); + } + location.reload(); + }); + } + }, _('Mount')) + ]) + ]); + }, + + // Create-partition dialog. Start sector is auto-detected (rounded UP to the + // next 2048-sector / 1 MiB boundary after the last partition) and a size + + // unit are converted to an end sector. The helper shows the max free space. + showCreatePartitionModal: function(devName, info) { + var secSize = (info && info.logic_sec) || 512; + var totalSectors = (info && info.size) ? Math.floor(info.size / secSize) : 0; + var maxUsable = totalSectors > 34 ? totalSectors - 34 : totalSectors; + var parts = (info && info.partitions) || []; + var GB = 1073741824, MB = 1048576; + + var maxEnd = -1; + parts.forEach(function(p) { if ((p.sec_end | 0) > maxEnd) maxEnd = p.sec_end | 0; }); + var autoStart = maxEnd >= 0 ? Math.ceil((maxEnd + 1) / 2048) * 2048 : 2048; + var remainBytes = Math.max(0, maxUsable - autoStart + 1) * secSize; + var remainFmt = formatBytes(remainBytes); + + var defaultUnit = 'GB', defaultVal = 1; + if (remainBytes >= GB) { defaultUnit = 'GB'; defaultVal = Math.min(50, Math.max(1, Math.floor(remainBytes / GB))); } + else if (remainBytes >= MB) { defaultUnit = 'MB'; defaultVal = Math.max(1, Math.floor(remainBytes / MB)); } + + var startIn = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'value': autoStart, 'style': 'width:140px;' }); + var sizeIn = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'value': defaultVal, 'style': 'width:140px;' }); + var unitSel = E('select', { 'class': 'cbi-input-select', 'style': 'width:100px;' }, [ + E('option', { 'value': 'TB' }, 'TB'), + E('option', { 'value': 'GB' }, 'GB'), + E('option', { 'value': 'MB' }, 'MB'), + E('option', { 'value': 'sectors' }, _('sectors')) + ]); + unitSel.value = defaultUnit; + var helper = E('div', { 'class': 'cbi-value-description' }, _('Maximum available: %s').format(remainFmt)); + + ui.showModal(_('Create Partition'), [ + E('p', { 'style': 'margin:0 0 1rem 0;' }, _('New partition on /dev/%s. Start sector auto-detected and aligned to 1 MiB.').format(devName)), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Start Sector')), + E('div', { 'class': 'cbi-value-field' }, startIn) + ]), + E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('Partition Size')), + E('div', { 'class': 'cbi-value-field' }, [ sizeIn, unitSel, helper ]) + ]), + E('div', { 'class': 'right', 'style': 'margin-top:1.5rem; display:flex; justify-content:flex-end; gap:8px;' }, [ + E('button', { 'class': 'btn cbi-button cbi-button-reset', 'click': ui.hideModal }, _('Cancel')), + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': function(ev) { + ev.preventDefault(); + var start = parseInt(startIn.value, 10); + var sizeVal = parseFloat(sizeIn.value); + var unit = unitSel.value; + if (isNaN(start) || start <= 0) { + ui.addNotification(null, E('p', {}, _('Invalid Start sector!'))); + return; + } + if (isNaN(sizeVal) || sizeVal <= 0) { + ui.addNotification(null, E('p', {}, _('Invalid Partition size!'))); + return; + } + var unitBytes = { 'TB': 1099511627776, 'GB': GB, 'MB': MB, 'sectors': secSize }[unit]; + var sectors = Math.max(1, Math.round((sizeVal * unitBytes) / secSize)); + var end = start + sectors - 1; + if (end > maxUsable) { + ui.addNotification(null, E('p', {}, _('Size exceeds the available free space.'))); + return; + } + this.disabled = true; + ui.showModal(_('Creating Partition'), [ E('p', { 'class': 'spinning' }, _('Creating new partition...')) ]); + callCreatePartition(devName, start, end, 'primary').then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to create partition.'))); + } + location.reload(); + }); + } + }, _('Create')) + ]) + ]); + }, + + // --- shared builders ---------------------------------------------------- + + // Colored, underlined SMART/health link (open the attributes modal on click). + buildSmartLink: function(devName, health) { + var raw = (health || '').trim().replace(/[-\s]+$/, '').trim(); + var cls = 'passed'; + if (raw && (raw.indexOf('Warning') !== -1 || raw.indexOf('Urgent') !== -1 || raw.indexOf('FAILED') !== -1)) cls = 'failed'; + else if (raw && raw.indexOf('Normal') !== -1) cls = 'passed'; + return E('a', { + 'href': '#', + 'class': 'smart-status-link ' + cls, + 'title': _('Click to view SMART & Health details'), + 'click': function(ev) { ev.preventDefault(); this.showSmartModal(devName); }.bind(this) + }, translateHealth(raw) || '-'); + }, + + // Bus/transport badge inferred from the device path. + busBadge: function(dev) { + var p = (dev.path || '').toLowerCase(); + if (p.indexOf('nvme') !== -1) return 'NVMe'; + if (p.indexOf('mmcblk') !== -1) return 'eMMC'; + if (p.indexOf('md') !== -1) return 'RAID'; + return 'SATA'; + }, + + // The 5-state legend strip. + buildLegend: function() { + var items = [ + ['part-state-boot', _('Boot / Firmware (protected)')], + ['part-state-inuse', _('Data (in use)')], + ['part-state-unmounted', _('Data (unmounted)')], + ['part-state-raw', _('Empty (no filesystem)')], + ['part-state-unalloc', _('Unallocated Space')] + ]; + return E('div', { 'class': 'disk-partition-legend' }, items.map(function(it) { + return E('span', { 'class': 'legend-item' }, [ + E('span', { 'class': 'legend-color-dot ' + it[0] }), + it[1] + ]); + })); + }, + + // The 14px pill partition bar. flex-grow still reflects relative size so a + // big data partition dwarfs the boot slivers, but each segment carries a CSS + // min-width so the partition name always fits — proportions stay + // approximate, legibility stays hard. Full detail via the hover tooltip. + buildPillBar: function(dev, parts) { + var totalSize = dev.size || 1; + var used = 0; + var segs = []; + function makeSeg(cls, pct, tipText, nameLabel) { + var grow = Math.max(1, Math.round(pct)); + return E('div', { + 'class': 'part-segment ' + cls, + 'style': 'flex:' + grow + ' 1 0;' + }, [ + nameLabel, + E('span', { 'class': 'part-tooltip' }, tipText) + ]); + } + if (!parts || parts.length === 0) { + var st = partState({ is_boot: dev.is_system, mount_point: dev.mount_point, fstype: dev.fstype }); + segs.push(makeSeg(st.cls, 100, dev.name + ' - ' + st.tag, dev.name)); + } else { + parts.forEach(function(p) { + used += (p.size || 0); + var st = partState(p); + var pct = (p.size / totalSize) * 100; + var tip = p.name + ' (' + (p.size_formated || '') + ') - ' + st.tag + + (p.fstype ? ' · ' + p.fstype : '') + + (p.mount_point && p.mount_point !== '-' ? ' -> ' + p.mount_point : ''); + segs.push(makeSeg(st.cls, pct, tip, p.name)); + }); + var unalloc = totalSize - used; + if (unalloc > totalSize * 0.015 && unalloc > 0) { + var upct = (unalloc / totalSize) * 100; + segs.push(makeSeg('part-state-unalloc', upct, + _('Unallocated') + ' (' + formatBytes(unalloc) + ')', _('Unallocated'))); + } + } + return E('div', { 'class': 'disk-partition-bar' }, segs); + }, + + // Compact overview card (physical-disks tab): header + pill bar. + buildDiskPartitionCard: function(dev) { + var self = this; + var parts = dev.partitions || []; + var card = E('div', { 'class': 'disk-partition-card' }); + + card.appendChild(E('div', { 'class': 'disk-part-card-header' }, [ + E('div', { 'class': 'disk-part-card-title' }, [ + E('span', { 'class': 'disk-bus-badge' }, this.busBadge(dev)), + E('span', { 'class': 'disk-part-path' }, dev.path), + E('span', { 'class': 'disk-part-model' }, dev.model || '') + ]), + E('span', { 'class': 'disk-part-spec' }, (dev.size_formated || '') + (dev.p_table && dev.p_table !== '-' ? ' · ' + dev.p_table : '')) + ])); + + card.appendChild(E('div', { 'class': 'disk-part-card-meta' }, [ + E('span', { 'class': 'disk-meta-tag' }, [_('Partitions: %d').format(parts.length)]), + E('span', { 'class': 'disk-meta-tag' }, [_('Serial: %s').format(dev.sn || '-')]) + ])); + + card.appendChild(this.buildPillBar(dev, parts)); + + return card; + }, + + // Detailed per-disk section (partition-management tab): header + bar/chips + + // master partition table + bottom action bar. All protection rules apply. + buildDiskCard: function(dev, info, data) { + var self = this; + var devName = dev.name; + var mountPoints = data[1] || []; + var formatCmds = data[4] || {}; + var parts = (info && info.partitions) || (dev.partitions) || []; + + var isSystem = !!(dev.is_system || (info && info.is_system)); + var anyMounted = parts.some(function(p) { return p.mount_point && p.mount_point !== '-'; }); + var hasPartitions = parts.some(function(p) { return p.number > 0; }); + var pTableVal = (info && info.p_table) || (dev.p_table || ''); + + var card = E('fieldset', { 'class': 'disk-card-section', 'id': 'disk-group-' + devName }); + + // Header (legend): identity left, size/table/SMART right. + card.appendChild(E('legend', {}, [ + E('span', { 'style': 'display:flex; align-items:center; gap:8px; flex-wrap:wrap;' }, [ + E('span', { 'class': 'disk-bus-badge' }, this.busBadge(dev)), + E('span', { 'style': 'font-size:14px; font-weight:600; color:var(--text-color-highest);' }, dev.path), + E('span', { 'class': 'disk-part-model' }, dev.model || '') + ]), + E('span', { 'style': 'display:flex; align-items:center; gap:12px; font-size:12px; color:var(--text-color-medium);' }, [ + ((info && info.size_formated) || (dev.size_formated || '')), + ((info && info.p_table) || (dev.p_table || '')), + this.buildSmartLink(devName, (info && info.health) || (dev.health_status)) + ]) + ])); + + // Pill bar only (the chips row was removed: the master partition table + // below already lists name / size / fs / mount for every partition). + var barWrap = E('div', { 'class': 'disk-card-bar-wrap' }); + barWrap.appendChild(this.buildPillBar(dev, parts)); + card.appendChild(barWrap); + + // Master partition table + var optByDev = {}; + mountPoints.forEach(function(mp) { optByDev[mp.device] = mp.mount_options; }); + + var partTable = E('table', { 'class': 'table cbi-section-table disk-card-table', 'style': 'min-width:1080px;' }, [ + E('colgroup', {}, [ + E('col', { 'style': 'width:11%;' }), + E('col', { 'style': 'width:13%;' }), + E('col', { 'style': 'width:16%;' }), + E('col', { 'style': 'width:8%;' }), + E('col', { 'style': 'width:9%;' }), + E('col', { 'style': 'width:15%;' }), + E('col', { 'style': 'width:7%;' }), + E('col', { 'style': 'width:21%;' }) + ]), + E('tr', { 'class': 'tr table-titles' }, [ + E('th', { 'class': 'th' }, _('Device')), + E('th', { 'class': 'th' }, _('File System')), + E('th', { 'class': 'th' }, _('Mount Point')), + E('th', { 'class': 'th' }, _('Size')), + E('th', { 'class': 'th' }, _('Usage')), + E('th', { 'class': 'th' }, _('Mount Options')), + E('th', { 'class': 'th' }, _('Auto Mount')), + E('th', { 'class': 'th' }, _('Actions')) + ]) + ]); + + parts.forEach(function(part) { + var isFree = (part.number === -1 || part.fs === 'Free Space' || part.type === 'free'); + var isMounted = part.mount_point && part.mount_point !== '-'; + var isSystemPart = !!part.is_boot; + var isExtended = (part.type === 'extended' || part.fs === 'extended'); + var opt = optByDev[part.path] || '—'; + + if (isFree) { + // Small firmware slivers (< 64 MiB) are not worth carving a new + // partition on, so we don't offer a "New" button on them. + var canCreate = (part.size || 0) >= MIN_MOUNT_BYTES; + partTable.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td', 'style': 'font-style:italic; color:var(--text-color-medium);' }, _('Free Space')), + E('td', { 'class': 'td' }, part.size_formated || '-'), + E('td', { 'class': 'td' }, '—'), + E('td', { 'class': 'td' }, part.size_formated || '-'), + E('td', { 'class': 'td' }, '—'), + E('td', { 'class': 'td' }, '—'), + E('td', { 'class': 'td' }, '—'), + E('td', { 'class': 'td cbi-section-actions' }, canCreate + ? E('button', { + 'class': 'btn cbi-button cbi-button-add', + 'title': _('Create a partition in this free region'), + 'click': function(ev) { ev.preventDefault(); self.showCreatePartitionModal(devName, info); } + }, _('New')) + : E('span', { 'class': 'label label-cbi-state-disabled' }, _('Unallocated'))) + ])); + return; + } + + var fsCell = E('span', { + 'title': isSystemPart ? _('System partition, protected') : null + }, translateFs(part.fs, part.type)); + + // Unmounted non-system partitions show a plain dash in the mount-point + // column - state is already obvious from the FS column and the pill bar. + // System/boot segments on a FIT-based eMMC are consumed by the bootloader + // and kernel (e.g. the FIT image) and are never mounted as a filesystem; + // a bare "—" here read as "unused", so spell the real role out instead. + var mountCell; + if (isMounted) { + mountCell = [ E('span', { 'class': 'badge badge-status badge-mounted' }, _('Mounted')), ' ', E('code', {}, part.mount_point) ]; + } else if (isSystemPart) { + mountCell = E('span', { + 'style': 'font-style:italic; color:var(--text-color-medium);', + 'title': _('Loaded by the bootloader and kernel from the FIT image; not mounted as a separate filesystem.') + }, _('Kernel-managed (FIT)')); + } else { + mountCell = '—'; + } + + var usageCell = (isMounted && part.usage) + ? E('div', { 'style': 'display:flex; align-items:center; gap:6px;' }, [ + E('div', { 'style': 'width:60px; height:6px; background:var(--background-color-medium); border-radius:3px; overflow:hidden;' }, [ + E('div', { 'style': 'width:' + part.usage + '; height:100%; background:var(--primary-color-high);' }) + ]), + E('span', { 'style': 'font-size:11px; color:var(--text-color-medium);' }, part.usage) + ]) + : '—'; + + // Unmounted partitions show a plain dash in the auto-mount column - + // labelling an unmounted partition "Disabled" read as contradictory. + var autoCell = isMounted + ? E('span', { 'style': 'color:var(--success-color-high); font-size:12px;' }, _('Enabled')) + : '—'; + + var actions = []; + if (isSystemPart) { + // System partitions are not operable at all. + actions.push(E('span', { 'class': 'label label-cbi-state-disabled' }, _('System partition, protected'))); + } else { + var hasLogicals = parts.some(function(p) { return p.type === 'logical'; }); + if (isMounted) { + actions.push(E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'title': _('Detach this partition from its mount point; data is kept'), + 'click': function(ev) { + ev.preventDefault(); + self.showConfirm(_('Unmount'), + _('Are you sure you want to unmount %s?').format(part.mount_point), + function() { + ui.showModal(_('Unmounting'), [ E('p', { 'class': 'spinning' }, _('Unmounting %s...').format(part.mount_point)) ]); + callUmount(part.mount_point).then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to unmount.'))); + } + location.reload(); + }); + }); + } + }, _('Unmount'))); + actions.push(E('button', { + 'class': 'btn cbi-button cbi-button-edit', + 'title': _('Change mount point / options'), + 'click': function(ev) { ev.preventDefault(); self.showMountModal(devName, dev, info, part, data); } + }, _('Edit Mount'))); + } else { + // Unmounted: Mount + Format (non-extended); Delete unless it's + // an extended container still holding logicals. + if (!isExtended) { + actions.push(E('button', { + 'class': 'btn cbi-button cbi-button-add', + 'title': _('Mount this partition'), + 'click': function(ev) { ev.preventDefault(); self.showMountModal(devName, dev, info, part, data); } + }, _('Mount'))); + } + if (!isExtended) { + actions.push(E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'title': _('Formatting erases all data on the partition'), + 'click': function(ev) { + ev.preventDefault(); + self.showFormatModal(part.name, formatCmds, part.fs, function() { location.reload(); }); + } + }, _('Format'))); + } + if (!(isExtended && hasLogicals)) { + actions.push(E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'title': _('Remove this partition from the table; its space becomes unallocated'), + 'click': function(ev) { + ev.preventDefault(); + self.showConfirm(_('Delete Partition'), + _('Are you sure you want to delete partition %s?').format(part.name), + function() { + ui.showModal(_('Deleting Partition'), [ E('p', { 'class': 'spinning' }, _('Removing partition...')) ]); + callRemovePartition(devName, part.number).then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to remove partition.'))); + } + location.reload(); + }); + }); + } + }, _('Delete'))); + } + } + } + + partTable.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td col-device' }, E('strong', {}, part.name || ('/' + devName + part.number))), + E('td', { 'class': 'td col-fs' }, fsCell), + E('td', { 'class': 'td col-mount' }, mountCell), + E('td', { 'class': 'td' }, part.size_formated || '-'), + E('td', { 'class': 'td' }, usageCell), + E('td', { 'class': 'td' }, E('span', { 'class': 'mount-options-text', 'title': opt }, opt)), + E('td', { 'class': 'td col-automount' }, autoCell), + E('td', { 'class': 'td cbi-section-actions' }, [ E('div', { 'class': 'btn-group-actions' }, actions) ]) + ])); + }); + + // (The inline quick-mount row was removed: every unmounted partition row + // already has a Mount button that opens showMountModal, which lists all + // mountable partitions on this disk — the inline row duplicated that.) + + card.appendChild(partTable); + + // Bottom action bar + var left = E('div', { 'class': 'disk-actions-left' }); + var right = E('div', { 'class': 'disk-actions-right' }); + if (!isSystem) { + left.appendChild(E('button', { + 'class': 'btn cbi-button cbi-button-add', + 'title': _('Create a new partition from the largest free region'), + 'click': function(ev) { ev.preventDefault(); self.showCreatePartitionModal(devName, info); } + }, _('New Partition'))); + left.appendChild(E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'disabled': anyMounted || null, + 'click': function(ev) { + ev.preventDefault(); + if (anyMounted) { + ui.addNotification(null, E('p', {}, _('Device is in use! Please unmount all partitions first!'))); + return; + } + self.showConfirm(_('Eject Device'), + _('Are you sure you want to eject/remove this device?'), + function() { + ui.showModal(_('Ejecting'), [ E('p', { 'class': 'spinning' }, _('Ejecting device...')) ]); + callEjectDevice(devName).then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to eject device.'))); + } + location.reload(); + }); + }); + } + }, _('Eject'))); + } + // The backend's mk_partition_table is `parted mklabel` (destructive wipe), + // so it is only offered on a disk with NO partitions yet - never on a disk + // that already has a layout (protects the eMMC / any real disk). + if (!hasPartitions && !isSystem && pTableVal && pTableVal.indexOf('Raid') === -1) { + var ptSel = E('select', { 'class': 'cbi-input-select' }, [ + E('option', { 'value': 'GPT' }, 'GPT'), + E('option', { 'value': 'MBR' }, 'MBR') + ]); + right.appendChild(ptSel); + right.appendChild(E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'title': _('Create a fresh GPT/MBR partition table'), + 'click': function(ev) { + ev.preventDefault(); + var label = ptSel.value === 'MBR' ? 'MBR' : 'GPT'; + self.showConfirm(_('Change Partition Table'), + _('Warning !!\nTHIS WILL OVERWRITE EXISTING PARTITIONS!!\nModify the partition table to %s?').format(label), + function() { + ui.showModal(_('Modifying Partition Table'), [ E('p', { 'class': 'spinning' }, _('Applying partition table...')) ]); + callMkPartitionTable(devName, ptSel.value).then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to modify partition table.'))); + } + location.reload(); + }); + }); + } + }, _('Apply Table'))); + } + + card.appendChild(E('div', { 'class': 'disk-card-actions-bar' }, [ + E('div', { 'class': 'disk-actions-flex' }, [ left, right ]) + ])); + + return card; + }, + + // --- RAID ----------------------------------------------------------------- + + buildRaidForm: function(data) { + var self = this; + var devices = data[0] || {}; + var form = E('fieldset', { 'class': 'cbi-section disk-card-section' }); + form.appendChild(E('legend', {}, [ + E('span', {}, [ + _('Create RAID'), + E('span', { 'style': 'font-size:12px; font-weight:normal; color:var(--text-color-medium); margin-left:8px;' }, + '(' + _('Select two or more free partitions to build a high-reliability or high-performance array') + ')') + ]) + ])); + var node = E('div', { 'class': 'cbi-section-node' }); + var nameIn = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'value': '/dev/md1', 'style': 'width:240px;' }); + var levelSel = E('select', { 'class': 'cbi-input-select', 'style': 'min-width:320px;' }, [ + E('option', { 'value': '1', 'selected': true }, 'RAID 1 ' + _('(Mirror - recommended dual-disk data fault tolerance, doubled read speed)')), + E('option', { 'value': '0' }, 'RAID 0 ' + _('(Stripe - high throughput, no redundancy, full capacity)')), + E('option', { 'value': '5' }, 'RAID 5 ' + _('(Distributed parity redundancy - needs at least 3 partitions)')), + E('option', { 'value': '6' }, 'RAID 6 ' + _('(Double parity fault tolerance - needs at least 4 partitions)')), + E('option', { 'value': '10' }, 'RAID 10 ' + _('(Mirror + Stripe hybrid - performance with high fault tolerance)')) + ]); + var chipGroup = buildMemberChips(devices, _('No free partitions available to select.')); + node.appendChild(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('RAID Name')), + E('div', { 'class': 'cbi-value-field' }, [ + nameIn, + E('div', { 'class': 'cbi-value-description', 'style': 'font-size:11px; color:var(--text-color-medium); margin-top:3px;' }, + _('Standard software RAID block device path, defaults to /dev/md1, /dev/md2...')) + ]) + ])); + node.appendChild(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('RAID Level')), + E('div', { 'class': 'cbi-value-field' }, levelSel) + ])); + node.appendChild(E('div', { 'class': 'cbi-value' }, [ + E('label', { 'class': 'cbi-value-title' }, _('RAID Members')), + E('div', { 'class': 'cbi-value-field' }, [ + chipGroup, + E('div', { 'class': 'cbi-value-description', 'style': 'font-size:11px; color:var(--text-color-medium); margin-top:4px;' }, + _('All currently unmounted free storage partitions are listed automatically.')) + ]) + ])); + form.appendChild(node); + var createBtn = E('button', { + 'class': 'btn cbi-button cbi-button-action important', + 'click': function(ev) { + ev.preventDefault(); + var members = []; + var boxes = chipGroup.querySelectorAll('input[type="checkbox"]'); + for (var i = 0; i < boxes.length; i++) { if (boxes[i].checked) members.push(boxes[i].value); } + if (members.length < 2) { + ui.addNotification(null, E('p', {}, _('Select at least two RAID members!'))); + return; + } + self.showConfirm(_('Create RAID'), + _('Creating a RAID array will ERASE the selected members. Continue?'), + function() { + ui.showModal(_('Creating RAID'), [ E('p', { 'class': 'spinning' }, _('Creating RAID device...')) ]); + callCreateRaid(nameIn.value, levelSel.value, members).then(function(res) { + if (res && res.code !== 0) { + ui.addNotification(null, E('p', {}, res.error || _('Failed to create RAID.'))); + } + location.reload(); + }); + }); + } + }, _('Create RAID')); + var resetBtn = E('button', { + 'class': 'btn cbi-button cbi-button-neutral', + 'click': function(ev) { ev.preventDefault(); if (typeof currentDraw === 'function') currentDraw(); } + }, _('Reset')); + form.appendChild(E('div', { 'class': 'cbi-page-actions', 'style': 'margin-top:16px; display:flex; gap:10px;' }, [ createBtn, resetBtn ])); + return form; + }, + + // --- Btrfs ---------------------------------------------------------------- + + buildBtrfsCard: function(pool, info, subs, data) { + var self = this; + var uuid = pool.uuid; + var mountPoints = data[1] || []; + var members = (pool.members || '').split(/\s*,\s*/).filter(Boolean); + var mp = btrfsMountPoint(mountPoints, pool.members); + var infoObj = info || {}; + var usagePct = parseFloat(infoObj.usage) || 0; + + var card = E('fieldset', { 'class': 'cbi-section disk-card-section btrfs-pool-card' }); + + // legend: Btrfs: '; + +/* ── CSS: inherit page theme ── */ +var CSS = '\ +*,*::before,*::after{box-sizing:border-box}\ +#hm-app{margin:0;padding:0;width:100%;color:inherit}\ +.hm-header{padding:6px 0 4px;display:flex;align-items:center;gap:10px}\ +.hm-header h2{margin:0;font-size:20px;font-weight:600;color:inherit}\ +.hm-header .sub{font-size:11px;opacity:.5;margin-top:2px;letter-spacing:.3px}\ +.hm-header>*,.hm-header h2,.hm-header .sub{background:transparent!important;box-shadow:none!important;border:none!important;border-radius:0!important;padding:0!important;margin:0!important}\ +.hm-tabs{display:flex;border-bottom:2px solid currentColor;border-bottom-color:rgba(128,128,128,.25);overflow:hidden}\ +.hm-tab{padding:10px 18px;font-size:13px;font-weight:500;opacity:.55;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-2px;transition:all .2s;white-space:nowrap;user-select:none;color:inherit}\ +.hm-tab:hover{opacity:.85}\ +.hm-tab.active{opacity:1;border-bottom-color:var(--primary,#5e72e4)}\ +.hm-panel{display:none;padding:20px 0}\ +.hm-panel.active{display:block}\ +.hm-cards{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:16px}\ +.hm-card{border:1px solid rgba(128,128,128,.2);border-radius:10px;padding:14px 16px}\ +.hm-card .lbl{font-size:11px;opacity:.55;letter-spacing:.3px}\ +.hm-card .val{font-size:22px;font-weight:700;margin-top:4px;line-height:1.2}\ +.hm-card .val.sm{font-size:15px;font-weight:600}\ +.hm-badge{display:inline-block;padding:3px 14px;border-radius:20px;font-size:12px;font-weight:600}\ +.hm-badge-run{background:#e8f5e9;color:#2e7d32}\ +.hm-badge-stop{background:#ffebee;color:#c62828}\ +.hm-badge-start{background:#fff8e1;color:#f57f17}\ +.hm-badge-off{opacity:.5}\ +.hm-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;vertical-align:middle}\ +.hm-dot-g{background:#2e7d32}.hm-dot-r{background:#c62828}.hm-dot-y{background:#f57f17}.hm-dot-x{background:#9e9e9e}\ +.hm-info{border:1px solid rgba(128,128,128,.2);border-radius:10px;padding:0;overflow:hidden;margin-bottom:20px}\ +.hm-info-title{padding:12px 18px;font-size:13px;font-weight:600;opacity:.7;border-bottom:1px solid rgba(128,128,128,.15)}\ +.hm-info table{width:100%;border-collapse:collapse}\ +.hm-info tr,.hm-info tr:nth-of-type(2n){background:transparent!important}\ +.hm-info td{padding:8px 16px;font-size:13px;border-bottom:1px solid rgba(128,128,128,.1)}\ +.hm-info tr:last-child td{border-bottom:none}\ +.hm-info td:first-child{width:100px;opacity:.6;font-weight:500}\ +.hm-info td:last-child{word-break:break-all}\ +.hm-actions{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:16px}\ +.hm-btn{padding:8px 16px;border:none;border-radius:8px;font-size:13px;font-weight:500;cursor:pointer;transition:all .15s;display:inline-flex;align-items:center;gap:6px}\ +.hm-btn:hover{filter:brightness(.93)}\ +.hm-btn:active{transform:scale(.97)}\ +.hm-btn:disabled{opacity:.5;cursor:not-allowed;transform:none;filter:none}\ +.hm-btn-p{background:var(--primary,#5e72e4);color:#fff}\ +.hm-btn-s{background:#1b5e20;color:#fff;box-shadow:0 2px 6px rgba(27,94,32,.35)}\ +.hm-btn-s:hover{background:#2e7d32}\ +.hm-btn-d{background:#37474f;color:#fff;box-shadow:0 2px 6px rgba(55,71,79,.3)}\ +.hm-btn-d:hover{background:#546e7a}\ +.hm-btn-r{background:#1565c0;color:#fff;box-shadow:0 2px 6px rgba(21,101,192,.35)}\ +.hm-btn-r:hover{background:#1976d2}\ +.hm-btn-g{border:1px solid rgba(128,128,128,.3);color:inherit;background:transparent}\ +.hm-btn-icon{padding:6px 10px;font-size:16px;line-height:1;min-width:unset}\ +.hm-log-wrap{margin-top:16px;display:none}\ +.hm-log-hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}\ +.hm-log-hdr span{font-weight:600;font-size:13px;opacity:.7}\ +.hm-log-st{font-size:12px}\ +.hm-log{background:#1e1e2e;color:#cdd6f4;padding:14px 16px;border-radius:8px;font-family:Consolas,Monaco,"Courier New",monospace;font-size:12px;line-height:1.6;max-height:380px;overflow-y:auto;white-space:pre-wrap;word-break:break-all;border:1px solid #313244}\ +.hm-log-result{margin-top:10px;padding:12px 16px;border-radius:8px;font-size:13px}\ +.hm-log-ok{background:#e8f5e9;border:1px solid #c8e6c9;color:#2e7d32}\ +.hm-log-fail{background:#ffebee;border:1px solid #ffcdd2;color:#c62828}\ +.hm-form{border:1px solid rgba(128,128,128,.2);border-radius:10px;overflow:hidden;margin-bottom:20px}\ +.hm-form-title{padding:12px 18px;font-size:13px;font-weight:600;opacity:.7;border-bottom:1px solid rgba(128,128,128,.15)}\ +.hm-form-body{padding:6px 18px}\ +.hm-form-row{display:flex;align-items:center;padding:12px 0;border-bottom:1px solid rgba(128,128,128,.1)}\ +.hm-form-row:last-child{border-bottom:none}\ +.hm-form-lbl{width:120px;font-size:13px;font-weight:500;opacity:.7;flex-shrink:0}\ +.hm-form-ctl{flex:1;min-width:0}\ +.hm-form-ctl input,.hm-form-ctl select{padding:7px 10px;border:1px solid rgba(128,128,128,.3);border-radius:6px;font-size:13px;width:100%;max-width:260px;outline:none;transition:border-color .2s;background:transparent;color:inherit}\ +.hm-form-ctl input:focus,.hm-form-ctl select:focus{border-color:var(--primary,#5e72e4)}\ +.hm-form-hint{font-size:11px;opacity:.5;margin-top:3px}\ +.hm-iframe-wrap{border:2px solid rgba(128,128,128,.2);border-radius:10px;overflow:hidden;margin-top:10px}\ +.hm-iframe-wrap iframe{width:100%;height:650px;border:none;display:block}\ +.hm-iframe-msg{padding:48px;text-align:center;opacity:.55;font-size:14px;line-height:1.8}\ +.hm-iframe-msg .icon{font-size:36px;margin-bottom:12px}\ +.hm-switch{position:relative;display:inline-block;width:44px;height:24px}\ +.hm-switch input{opacity:0;width:0;height:0}\ +.hm-switch .slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:rgba(128,128,128,.35);border-radius:24px;transition:.3s}\ +.hm-switch .slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s}\ +.hm-switch input:checked+.slider{background:var(--primary,#5e72e4)}\ +.hm-switch input:checked+.slider:before{transform:translateX(20px)}\ +.hm-more-wrap{position:relative;display:inline-block}\ +.hm-more-menu{position:fixed;border:1px solid rgba(128,128,128,.2);border-radius:10px;box-shadow:0 -4px 20px rgba(0,0,0,.15);min-width:190px;z-index:9999;display:none;overflow:hidden;padding:4px 0;background:var(--background-color-low,#f8f8f8)}\ +.hm-more-item{display:flex;align-items:center;gap:8px;padding:10px 16px;font-size:13px;cursor:pointer;border:none;background:none;width:100%;text-align:left;transition:background .12s;color:inherit}\ +.hm-more-item:hover{background:rgba(128,128,128,.1)}\ +.hm-more-item.danger{color:inherit;background:none}\ +.hm-more-item.danger:hover{background:rgba(128,128,128,.1)}\ +.hm-more-sep{height:1px;background:rgba(128,128,128,.15);margin:4px 0}\ +@media(max-width:768px){\ +#hm-app{padding:0 10px;overflow-x:hidden}\ +.hm-tabs{max-width:100%}\ +.hm-header{padding:8px 0;gap:10px}\ +.hm-header h2{font-size:16px}\ +.hm-cards{grid-template-columns:repeat(2,1fr);gap:10px}\ +.hm-card .val{font-size:17px}\ +.hm-card .val.sm{font-size:13px}\ +.hm-actions{gap:6px;flex-wrap:wrap;max-width:100%}\ +.hm-actions>*{flex:0 1 auto;min-width:0}\ +.hm-actions .hm-btn{padding:7px 10px;font-size:12px;white-space:nowrap;justify-content:center}\ +.hm-form-row{flex-direction:column;align-items:stretch;gap:4px}\ +.hm-form-lbl{width:auto}\ +.hm-form-ctl input,.hm-form-ctl select{max-width:100%}\ +.hm-iframe-wrap iframe{height:400px}\ +.hm-info td:first-child{width:80px}\ +}\ +@media(max-width:420px){\ +.hm-header{padding:6px 0}\ +.hm-header h2{font-size:15px}\ +.hm-tab{padding:9px 14px;font-size:12px}\ +.hm-btn{padding:6px 10px;font-size:12px}\ +.hm-actions .hm-btn{padding:6px 8px;font-size:11px}\ +.hm-info td{padding:7px 12px;font-size:12px}\ +.hm-cards{gap:8px}\ +.hm-card{padding:10px 12px}\ +.hm-card .val{font-size:16px}\ +.hm-iframe-wrap iframe{height:350px}\ +}\ +'; + +return view.extend({ + + load: function() { + return Promise.all([ + uci.load('hermes'), + callHelper(['status']) + ]); + }, + + render: function(data) { + var st = data[1] || {}; + this._st = st; + this._setupTimer = null; + this._tabEls = {}; + + var contentEl = E('div', { 'id': 'hm-content' }, [ + this._overview(st), + this._settings(), + this._console(st), + this._terminal(st) + ]); + this._contentEl = contentEl; + + var app = E('div', { 'id': 'hm-app' }, [ + E('style', {}, [CSS]), + this._header(), + this._tabBar(), + contentEl + ]); + + this._switchTab('overview'); + poll.add(L.bind(this._poll, this), 5); + + document.addEventListener('click', function() { + var m = document.getElementById('hm-more-menu'); + if (m) m.style.display = 'none'; + }); + + return app; + }, + + /* ═══ Header ═══ */ + _header: function() { + var h = E('div', { 'class': 'hm-header' }); + h.innerHTML = LOGO_SVG + + '

Hermes Agent

' + + '
' + _('Python AI Agent Gateway for OpenWrt') + '
'; + return h; + }, + + /* ═══ Tabs ═══ */ + _tabBar: function() { + var self = this; + var tabs = [ + ['overview', _('Overview')], + ['settings', _('Settings')], + ['console', _('Web Console')], + ['terminal', _('Config Terminal')] + ]; + var bar = E('div', { 'class': 'hm-tabs' }); + tabs.forEach(function(t) { + var el = E('div', { + 'class': 'hm-tab', + 'data-tab': t[0], + 'click': function() { self._switchTab(t[0]); } + }, [t[1]]); + self._tabEls[t[0]] = el; + bar.appendChild(el); + }); + return bar; + }, + + _switchTab: function(id) { + var self = this; + Object.keys(this._tabEls).forEach(function(k) { + self._tabEls[k].classList.toggle('active', k === id); + }); + var root = this._contentEl || document.getElementById('hm-content'); + if (root) { + var panels = root.querySelectorAll('.hm-panel'); + if (panels) panels.forEach(function(p) { + p.classList.toggle('active', p.getAttribute('data-panel') === id); + }); + } + }, + + /* ═══ Overview Panel ═══ */ + _overview: function(st) { + return E('div', { 'class': 'hm-panel', 'data-panel': 'overview' }, [ + /* Status Cards */ + E('div', { 'class': 'hm-cards' }, [ + this._card('status', _('Service Status'), this._badge(st)), + this._card('port', _('Gateway Port'), st.port || '3000', true), + this._card('memory', _('Memory'), fmtMem(st.memory_kb), true), + this._card('uptime', _('Uptime'), st.uptime || '-', true) + ]), + /* Version Info */ + this._infoTable(st), + /* Action Buttons */ + this._actionBtns(), + /* Log Viewer */ + this._logViewer() + ]); + }, + + _card: function(id, label, valueHtml, small) { + var c = E('div', { 'class': 'hm-card' }, [ + E('div', { 'class': 'lbl' }, [label]), + E('div', { 'class': 'val' + (small ? ' sm' : ''), 'id': 'hm-c-' + id }) + ]); + if (typeof valueHtml === 'string' && valueHtml.indexOf('<') >= 0) + c.querySelector('.val').innerHTML = valueHtml; + else + c.querySelector('.val').textContent = valueHtml || '-'; + return c; + }, + + _badge: function(st) { + if (!st || !st.enabled) return '' + _('Unknown') + ''; + if (st.enabled !== '1') return '' + _('Disabled') + ''; + if (st.gateway_running) return '' + _('Running') + ''; + if (st.gateway_starting) return '' + _('Starting') + ''; + return '' + _('Stopped') + ''; + }, + + _infoTable: function(st) { + var rows = [ + [_('Python'), 'hm-i-python', st.python_version || _('Not installed')], + [_('Hermes'), 'hm-i-hermes', st.hermes_version || _('Not installed')], + [_('Plugin'), 'hm-i-plugin', st.plugin_version || '-'], + [_('Model'), 'hm-i-model', st.active_model || '-'], + [_('PID'), 'hm-i-pid', st.pid || '-'], + [_('Config PTY'), 'hm-i-pty', st.pty_running ? '✅ ' + _('Running') + ' (:' + (st.pty_port || '3001') + ')' : '⏹ ' + _('Stopped')] + ]; + var tbody = E('tbody'); + rows.forEach(function(r) { + tbody.appendChild(E('tr', {}, [ + E('td', {}, [r[0]]), + E('td', { 'id': r[1] }, [r[2]]) + ])); + }); + return E('div', { 'class': 'hm-info' }, [ + E('div', { 'class': 'hm-info-title' }, [_('System Information')]), + E('table', {}, [tbody]) + ]); + }, + + _actionBtns: function() { + var self = this; + var wrap = E('div', { 'class': 'hm-actions' }); + + wrap.appendChild(E('button', { + 'class': 'hm-btn hm-btn-s', 'id': 'hm-btn-start', + 'click': function() { self._svcCtl('start'); } + }, ['▶ ' + _('Start')])); + + wrap.appendChild(E('button', { + 'class': 'hm-btn hm-btn-d', 'id': 'hm-btn-stop', + 'click': function() { self._svcCtl('stop'); } + }, ['⏹ ' + _('Stop')])); + + wrap.appendChild(E('button', { + 'class': 'hm-btn hm-btn-r', 'id': 'hm-btn-restart', + 'click': function() { self._svcCtl('restart'); } + }, ['🔄 ' + _('Restart')])); + + /* More menu */ + var gearWrap = E('div', { 'class': 'hm-more-wrap' }); + gearWrap.appendChild(E('button', { + 'class': 'hm-btn hm-btn-g hm-btn-icon', 'title': _('More'), + 'click': function(ev) { + ev.stopPropagation(); + var m = document.getElementById('hm-more-menu'); + if (!m) return; + if (m.style.display === 'block') { m.style.display = 'none'; return; } + var rect = ev.currentTarget.getBoundingClientRect(); + m.style.display = 'block'; + var mh = m.offsetHeight; + m.style.top = (rect.top - mh - 6) + 'px'; + m.style.left = 'auto'; + m.style.right = (window.innerWidth - rect.right) + 'px'; + } + }, ['⚙'])); + + gearWrap.appendChild(E('div', { 'class': 'hm-more-menu', 'id': 'hm-more-menu' }, [ + E('div', { 'class': 'hm-more-item', 'click': function() { self._closeMenu(); self._showSetupDialog(); } }, ['📦 ' + _('Install Environment')]), + E('div', { 'class': 'hm-more-sep' }), + E('div', { 'class': 'hm-more-item', 'click': function() { self._closeMenu(); self._uninstall(); } }, ['🗑️ ' + _('Uninstall')]) + ])); + + wrap.appendChild(gearWrap); + return wrap; + }, + + _closeMenu: function() { + var m = document.getElementById('hm-more-menu'); + if (m) m.style.display = 'none'; + }, + + _logViewer: function() { + return E('div', { 'class': 'hm-log-wrap', 'id': 'hm-log-wrap' }, [ + E('div', { 'class': 'hm-log-hdr' }, [ + E('span', { 'id': 'hm-log-title' }, ['📋 ' + _('Log')]), + E('span', { 'class': 'hm-log-st', 'id': 'hm-log-st' }) + ]), + E('pre', { 'class': 'hm-log', 'id': 'hm-log' }), + E('div', { 'id': 'hm-log-result' }) + ]); + }, + + /* ═══ Settings Panel ═══ */ + _settings: function() { + var self = this; + var enabled = uci.get('hermes', 'main', 'enabled') === '1'; + var port = uci.get('hermes', 'main', 'port') || '3000'; + var bind = uci.get('hermes', 'main', 'bind') || 'lan'; + var ptyPort = uci.get('hermes', 'main', 'pty_port') || '3001'; + var apiEndpoint = uci.get('hermes', 'main', 'api_endpoint') || 'https://api.boos.lat/v1'; + var apiKey = uci.get('hermes', 'main', 'api_key') || ''; + var model = uci.get('hermes', 'main', 'model') || 'opus4.6'; + + return E('div', { 'class': 'hm-panel', 'data-panel': 'settings' }, [ + E('div', { 'class': 'hm-form' }, [ + E('div', { 'class': 'hm-form-title' }, [_('Basic Settings')]), + E('div', { 'class': 'hm-form-body' }, [ + this._formRow(_('Enable Service'), this._toggle('hm-f-enabled', enabled)), + this._formRow(_('Gateway Port'), this._input('hm-f-port', port, 'number', _('Default: 3000'))), + this._formRow(_('Listen Interface'), this._select('hm-f-bind', bind, [ + ['lan', 'LAN'], + ['loopback', 'Loopback'], + ['all', _('All Interfaces')] + ])), + this._formRow(_('PTY Port'), this._input('hm-f-pty-port', ptyPort, 'number', _('Default: 3001'))) + ]) + ]), + E('div', { 'class': 'hm-form' }, [ + E('div', { 'class': 'hm-form-title' }, [_('API Settings')]), + E('div', { 'class': 'hm-form-body' }, [ + this._formRow(_('API Endpoint'), this._input('hm-f-api-endpoint', apiEndpoint, 'text', _('Default: https://api.boos.lat/v1'))), + this._formRow(_('API Key'), this._input('hm-f-api-key', apiKey, 'password', _('Your API key'))), + this._formRow(_('Model'), this._input('hm-f-model', model, 'text', _('Default: opus4.6'))) + ]) + ]), + E('div', { 'class': 'hm-actions' }, [ + E('button', { 'class': 'hm-btn hm-btn-p', 'click': function() { self._saveSettings(); } }, ['💾 ' + _('Save & Apply')]), + E('button', { 'class': 'hm-btn hm-btn-g', 'click': function() { location.reload(); } }, [_('Reset')]) + ]) + ]); + }, + + _formRow: function(label, control) { + return E('div', { 'class': 'hm-form-row' }, [ + E('div', { 'class': 'hm-form-lbl' }, [label]), + E('div', { 'class': 'hm-form-ctl' }, [control]) + ]); + }, + + _toggle: function(id, checked) { + var lbl = E('label', { 'class': 'hm-switch' }); + lbl.innerHTML = ''; + return lbl; + }, + + _input: function(id, value, type, hint) { + var wrap = E('div'); + wrap.appendChild(E('input', { 'type': type || 'text', 'id': id, 'value': value })); + if (hint) wrap.appendChild(E('div', { 'class': 'hm-form-hint' }, [hint])); + return wrap; + }, + + _select: function(id, value, opts) { + var sel = E('select', { 'id': id }); + opts.forEach(function(o) { + var opt = E('option', { 'value': o[0] }, [o[1]]); + if (o[0] === value) opt.setAttribute('selected', 'selected'); + sel.appendChild(opt); + }); + return sel; + }, + + /* ═══ Console Panel ═══ */ + _console: function(st) { + var panel = E('div', { 'class': 'hm-panel', 'data-panel': 'console' }); + var container = E('div', { 'class': 'hm-iframe-wrap' }); + + if (st.gateway_running) { + var proto = window.location.protocol; + var host = window.location.hostname; + var base = proto + '//' + host + ':' + (st.port || '3000') + '/'; + + container.innerHTML = '
⏳
' + _('Connecting...') + '
'; + callHelper(['get_token']).then(function(tok) { + var url = base; + if (tok && tok.token) url += '?token=' + encodeURIComponent(tok.token); + container.innerHTML = ''; + container.appendChild(E('iframe', { + 'src': url, + 'id': 'hm-console-iframe', + 'allow': 'clipboard-read; clipboard-write', + 'style': 'width:100%;height:650px;border:none;display:block', + 'loading': 'lazy' + })); + }); + } else { + container.innerHTML = '
' + + '
🖥️
' + + '
' + _('Web console is not available.') + '
' + + '
' + _('Please start the Hermes service first.') + '
'; + } + panel.appendChild(container); + return panel; + }, + + /* ═══ Terminal Panel ═══ */ + _terminal: function(st) { + var panel = E('div', { 'class': 'hm-panel', 'data-panel': 'terminal' }); + var container = E('div', { 'class': 'hm-iframe-wrap' }); + + if (st.pty_running) { + var proto = window.location.protocol; + var host = window.location.hostname; + var ptyPort = st.pty_port || '3001'; + var url = proto + '//' + host + ':' + ptyPort + '/'; + + container.innerHTML = '
' + + '
⏳
' + + '
' + _('Connecting to config terminal...') + '
'; + callHelper(['get_token']).then(function(tok) { + if (tok && tok.pty_token) + url += '?token=' + encodeURIComponent(tok.pty_token); + container.innerHTML = ''; + container.appendChild(E('iframe', { + 'src': url, + 'allow': 'clipboard-read; clipboard-write', + 'style': 'width:100%;height:650px;border:none;display:block', + 'loading': 'lazy' + })); + }); + } else { + container.innerHTML = '
' + + '
⌨️
' + + '
' + _('Config terminal is not running.') + '
' + + '
' + _('Please start the Hermes service first.') + '
'; + } + panel.appendChild(container); + return panel; + }, + + /* ═══ Status Polling ═══ */ + _poll: function() { + var self = this; + return callHelper(['status']).then(function(st) { + self._st = st; + self._updateDisplay(st); + }); + }, + + _updateDisplay: function(st) { + var el; + /* Status card */ + el = document.getElementById('hm-c-status'); + if (el) el.innerHTML = this._badge(st); + /* Port */ + el = document.getElementById('hm-c-port'); + if (el) el.textContent = st.port || '3000'; + /* Memory */ + el = document.getElementById('hm-c-memory'); + if (el) el.textContent = fmtMem(st.memory_kb); + /* Uptime */ + el = document.getElementById('hm-c-uptime'); + if (el) el.textContent = st.uptime || '-'; + /* Info rows */ + var map = { + 'hm-i-python': st.python_version || _('Not installed'), + 'hm-i-hermes': st.hermes_version || _('Not installed'), + 'hm-i-plugin': st.plugin_version || '-', + 'hm-i-model': st.active_model || '-', + 'hm-i-pid': st.pid || '-', + 'hm-i-pty': st.pty_running ? '✅ ' + _('Running') + ' (:' + (st.pty_port || '3001') + ')' : '⏹ ' + _('Stopped') + }; + Object.keys(map).forEach(function(id) { + el = document.getElementById(id); + if (el) el.textContent = map[id]; + }); + /* Button states */ + var startBtn = document.getElementById('hm-btn-start'); + var stopBtn = document.getElementById('hm-btn-stop'); + if (startBtn) startBtn.disabled = !!st.gateway_running; + if (stopBtn) stopBtn.disabled = !st.gateway_running; + }, + + /* ═══ Service Control ═══ */ + _svcCtl: function(action) { + var self = this; + ui.showModal(_('Service Control'), [ + E('p', {}, [_('Executing: ') + action + '...']), + E('div', { 'class': 'spinning' }) + ]); + return fs.exec('/etc/init.d/hermes', [action]).then(function() { + return new Promise(function(resolve) { window.setTimeout(resolve, 2500); }); + }).then(function() { + return self._poll(); + }).then(function() { + ui.hideModal(); + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, [_('Error: ') + (e.message || e)])); + }); + }, + + /* ═══ Setup Dialog ═══ */ + _showSetupDialog: function() { + var self = this; + + var overlay = E('div', { 'class': 'hm-dialog-overlay', 'id': 'hm-setup-dlg' }); + var dlg = E('div', { 'class': 'hm-dialog' }, [ + E('h3', {}, ['📦 ' + _('Install Environment')]), + E('div', { 'class': 'hm-dialog-info' }, [ + E('p', {}, [_('This will install Python 3 and Hermes Agent via pip.')]), + E('p', {}, [_('Requires internet connection and sufficient disk space (~100MB).')]) + ]), + E('div', { 'class': 'hm-dialog-btns' }, [ + E('button', { 'class': 'hm-btn hm-btn-g', 'click': function() { overlay.remove(); } }, [_('Cancel')]), + E('button', { 'class': 'hm-btn hm-btn-p', 'click': function() { overlay.remove(); self._doSetup(); } }, [_('Install')]) + ]) + ]); + overlay.appendChild(dlg); + document.body.appendChild(overlay); + }, + + _doSetup: function() { + var self = this; + var logWrap = document.getElementById('hm-log-wrap'); + var logEl = document.getElementById('hm-log'); + var stEl = document.getElementById('hm-log-st'); + var resultEl = document.getElementById('hm-log-result'); + + if (logWrap) logWrap.style.display = 'block'; + if (logEl) logEl.textContent = _('Starting installation') + '...\n'; + if (stEl) stEl.innerHTML = '⏳ ' + _('Installing...') + ''; + if (resultEl) { resultEl.innerHTML = ''; resultEl.className = ''; } + + callHelper(['setup']).then(function() { + self._pollSetupLog(); + }); + }, + + _pollSetupLog: function() { + var self = this; + var lastLen = 0; + if (this._setupTimer) clearInterval(this._setupTimer); + + this._setupTimer = setInterval(function() { + callHelper(['setup_log']).then(function(r) { + var logEl = document.getElementById('hm-log'); + var stEl = document.getElementById('hm-log-st'); + var resultEl = document.getElementById('hm-log-result'); + if (!logEl) return; + + if (r.log && r.log.length > lastLen) { + logEl.textContent += r.log.substring(lastLen); + lastLen = r.log.length; + } + logEl.scrollTop = logEl.scrollHeight; + + if (r.state === 'running') { + if (stEl) stEl.innerHTML = '⏳ ' + _('Installing...') + ''; + } else if (r.state === 'success') { + clearInterval(self._setupTimer); + if (stEl) stEl.innerHTML = '✅ ' + _('Complete') + ''; + if (resultEl) { + resultEl.className = 'hm-log-result hm-log-ok'; + resultEl.innerHTML = '🎉 ' + _('Installation successful!') + '
' + + '' + _('Refresh the page to see updated status.') + '' + + '
'; + } + } else if (r.state === 'failed') { + clearInterval(self._setupTimer); + if (stEl) stEl.innerHTML = '❌ ' + _('Failed') + ''; + if (resultEl) { + resultEl.className = 'hm-log-result hm-log-fail'; + resultEl.textContent = '❌ ' + _('Installation failed. Check the log above for details.'); + } + } + }); + }, 1500); + }, + + /* ═══ Uninstall ═══ */ + _uninstall: function() { + var self = this; + ui.showModal(_('Confirm Uninstall'), [ + E('p', { 'style': 'color:#c62828' }, [ + '⚠️ ' + _('This will remove Python, Hermes Agent, and all related data.') + ]), + E('p', {}, [_('This action cannot be undone.')]), + E('div', { 'style': 'display:flex;gap:10px;justify-content:flex-end;margin-top:16px' }, [ + E('button', { 'class': 'hm-btn hm-btn-g', 'click': function() { ui.hideModal(); } }, [_('Cancel')]), + E('button', { 'class': 'hm-btn hm-btn-d', 'click': function() { + ui.hideModal(); + ui.showModal(_('Uninstalling...'), [E('div', { 'class': 'spinning' })]); + callHelper(['uninstall']).then(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, ['✅ ' + _('Environment uninstalled successfully')])); + return self._poll(); + }); + } }, ['🗑️ ' + _('Confirm Uninstall')]) + ]) + ]); + }, + + /* ═══ Save Settings ═══ */ + _saveSettings: function() { + var self = this; + var enabled = document.getElementById('hm-f-enabled'); + var port = document.getElementById('hm-f-port'); + var bind = document.getElementById('hm-f-bind'); + var ptyPort = document.getElementById('hm-f-pty-port'); + var apiEndpoint = document.getElementById('hm-f-api-endpoint'); + var apiKey = document.getElementById('hm-f-api-key'); + var model = document.getElementById('hm-f-model'); + + uci.set('hermes', 'main', 'enabled', enabled && enabled.checked ? '1' : '0'); + if (port) uci.set('hermes', 'main', 'port', port.value); + if (bind) uci.set('hermes', 'main', 'bind', bind.value); + if (ptyPort) uci.set('hermes', 'main', 'pty_port', ptyPort.value); + if (apiEndpoint) uci.set('hermes', 'main', 'api_endpoint', apiEndpoint.value); + if (apiKey) uci.set('hermes', 'main', 'api_key', apiKey.value); + if (model) uci.set('hermes', 'main', 'model', model.value); + + return uci.save().then(function() { + return uci.apply(); + }).then(function() { + ui.addNotification(null, E('p', {}, ['✅ ' + _('Settings saved successfully')])); + return self._poll(); + }).catch(function(e) { + ui.addNotification(null, E('p', {}, [_('Error: ') + (e.message || e)])); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-hermes/luasrc/controller/hermes.lua b/luci-app-hermes/luasrc/controller/hermes.lua index 45b3c901d..c6fd968c8 100644 --- a/luci-app-hermes/luasrc/controller/hermes.lua +++ b/luci-app-hermes/luasrc/controller/hermes.lua @@ -1,6 +1,78 @@ +-- luci-app-hermes — LuCI Controller (18.06 compat) module("luci.controller.hermes", package.seeall) function index() - entry({"admin", "services", "hermes"}, alias("admin", "services", "hermes", "config"), _("Hermes"), 30).dependent = true - entry({"admin", "services", "hermes", "config"}, cbi("hermes")) + -- luci 24.10+ uses menu.d JSON for registration; skip Lua menu + if nixio.fs.access("/usr/share/luci/menu.d/luci-app-hermes.json") then + return + end + + local page = entry({"admin", "services", "hermes"}, + alias("admin", "services", "hermes", "basic"), _("Hermes Agent"), 90) + page.dependent = false + + entry({"admin", "services", "hermes", "basic"}, + cbi("hermes"), _("Basic Settings"), 10).leaf = true + entry({"admin", "services", "hermes", "console"}, + template("hermes/console"), _("Web Console"), 20).leaf = true + entry({"admin", "services", "hermes", "terminal"}, + template("hermes/terminal"), _("Config Terminal"), 30).leaf = true + + -- API endpoints (used by status.htm XHR) + entry({"admin", "services", "hermes", "status_api"}, + call("action_status"), nil).leaf = true + entry({"admin", "services", "hermes", "service_ctl"}, + call("action_service_ctl"), nil).leaf = true + entry({"admin", "services", "hermes", "get_token"}, + call("action_get_token"), nil).leaf = true +end + +-- ── Status API ── +function action_status() + local http = require "luci.http" + local sys = require "luci.sys" + + local raw = sys.exec("/usr/share/hermes/luci-helper status 2>/dev/null") + http.prepare_content("application/json") + http.write(raw ~= "" and raw or '{}') +end + +-- ── Service Control API ── +function action_service_ctl() + local http = require "luci.http" + local sys = require "luci.sys" + + local action = http.formvalue("action") or "" + local allowed = { start = true, stop = true, restart = true, enable = true, disable = true } + + if not allowed[action] then + http.prepare_content("application/json") + http.write_json({ status = "error", message = "invalid action" }) + return + end + + if action == "stop" then + sys.exec("/etc/init.d/hermes stop >/dev/null 2>&1") + elseif action == "restart" then + sys.exec("/etc/init.d/hermes stop >/dev/null 2>&1") + sys.exec("sleep 2") + sys.exec("/etc/init.d/hermes start >/dev/null 2>&1 &") + else + sys.exec("/etc/init.d/hermes " .. action .. " >/dev/null 2>&1 &") + end + + http.prepare_content("application/json") + http.write_json({ status = "ok", action = action }) +end + +-- ── Get Token API ── +function action_get_token() + local http = require "luci.http" + local uci = require "luci.model.uci".cursor() + + http.prepare_content("application/json") + http.write_json({ + token = uci:get("hermes", "main", "token") or "", + pty_token = uci:get("hermes", "main", "pty_token") or "" + }) end diff --git a/luci-app-hermes/luasrc/model/cbi/hermes.lua b/luci-app-hermes/luasrc/model/cbi/hermes.lua index d81bd1582..9b7f042ea 100644 --- a/luci-app-hermes/luasrc/model/cbi/hermes.lua +++ b/luci-app-hermes/luasrc/model/cbi/hermes.lua @@ -1,45 +1,52 @@ ---[[ -LuCI - Lua Configuration Interface -]]-- +-- luci-app-hermes — CBI Model (18.06 compat) +local sys = require "luci.sys" -local taskd = require "luci.model.tasks" -local hermes_model = require "luci.model.hermes" -local m, s, o +m = Map("hermes", "Hermes Agent", -m = taskd.docker_map("hermes", "hermes", "/usr/libexec/istorec/hermes.sh", - translate("Hermes"), - translate("The self-improving AI agent that runs on your server. Layered memory that accumulates across sessions, a cron scheduler that fires while you're offline, and a skills system that saves reusable procedures automatically.") - .. translate("Official website:") .. ' https://get-hermes.ai/') + translate("Python-based AI Agent Gateway for OpenWrt routers.")) -s = m:section(SimpleSection, translate("Service Status"), translate("Hermes status:")) -s:append(Template("hermes/status")) +-- Status panel +m:section(SimpleSection).template = "hermes/status" -s = m:section(TypedSection, "main", translate("Setup"), translate("The following parameters will only take effect during installation or upgrade:")) +-- Basic settings +s = m:section(NamedSection, "main", "hermes", translate("Basic Settings")) s.addremove = false s.anonymous = true -o = s:option(Value, "port", translate("Port") .. "*") -o.default = "8787" +o = s:option(Flag, "enabled", translate("Enable Service")) +o.rmempty = false + +o = s:option(Value, "port", translate("Gateway Port")) o.datatype = "port" - -local blocks = hermes_model.blocks() - -o = s:option(Value, "data_path", translate("Data path") .. "*") +o.default = "3000" o.rmempty = false -o.datatype = "string" -local data_paths, data_default = hermes_model.find_paths(blocks, "data") -for _, val in pairs(data_paths) do - o:value(val, val) -end -o.default = data_default -o = s:option(Value, "workspace_path", translate("Workspace path") .. "*") +o = s:option(ListValue, "bind", translate("Listen Interface")) +o:value("lan", "LAN") +o:value("loopback", "Loopback") +o:value("all", translate("All Interfaces")) +o.default = "lan" + +o = s:option(Value, "pty_port", translate("PTY Port")) +o.datatype = "port" +o.default = "3001" +o.rmempty = false + +-- API settings +s2 = m:section(NamedSection, "main", "hermes", translate("API Settings")) +s2.addremove = false +s2.anonymous = true + +o = s2:option(Value, "api_endpoint", translate("API Endpoint")) +o.default = "https://api.boos.lat/v1" +o.rmempty = false + +o = s2:option(Value, "api_key", translate("API Key")) +o.password = true +o.rmempty = true + +o = s2:option(Value, "model", translate("Model")) +o.default = "opus4.6" o.rmempty = false -o.datatype = "string" -local ws_paths, ws_default = hermes_model.find_paths(blocks, "workspace") -for _, val in pairs(ws_paths) do - o:value(val, val) -end -o.default = ws_default return m diff --git a/luci-app-hermes/luasrc/model/hermes.lua b/luci-app-hermes/luasrc/model/hermes.lua deleted file mode 100644 index a1c34d199..000000000 --- a/luci-app-hermes/luasrc/model/hermes.lua +++ /dev/null @@ -1,38 +0,0 @@ -local util = require "luci.util" -local jsonc = require "luci.jsonc" - -local hermes = {} - -hermes.blocks = function() - local f = io.popen("lsblk -s -f -b -o NAME,FSSIZE,MOUNTPOINT --json", "r") - local vals = {} - if f then - local ret = f:read("*all") - f:close() - local obj = jsonc.parse(ret) - if obj and obj["blockdevices"] then - for _, val in pairs(obj["blockdevices"]) do - local fsize = val["fssize"] - if fsize ~= nil and string.len(fsize) > 10 and val["mountpoint"] then - -- fsize > 1G - vals[#vals+1] = val["mountpoint"] - end - end - end - end - return vals -end - -hermes.find_paths = function(blocks, subdir) - local configs = {} - local default_path = '' - for _, mountpoint in pairs(blocks) do - table.insert(configs, mountpoint .. "/hermes/" .. subdir) - end - if #configs > 0 then - default_path = configs[1] - end - return configs, default_path -end - -return hermes diff --git a/luci-app-hermes/luasrc/view/hermes/console.htm b/luci-app-hermes/luasrc/view/hermes/console.htm new file mode 100644 index 000000000..4daf6895a --- /dev/null +++ b/luci-app-hermes/luasrc/view/hermes/console.htm @@ -0,0 +1,59 @@ +<%# + luci-app-hermes — Web Console (18.06 compat) +-%> +<%+header%> +<% +local uci = require "luci.model.uci".cursor() +local port = uci:get("hermes", "main", "port") or "3000" +%> + + +
+

🖥️ <%:Web Console%>

+

<%:Hermes Agent Web UI — configure AI models and interact with the AI agent.%>

+
+ +
+
⏳ <%:Connecting...%>
+
+ + +<%+footer%> diff --git a/luci-app-hermes/luasrc/view/hermes/status.htm b/luci-app-hermes/luasrc/view/hermes/status.htm index 4884d4e45..31c1b011b 100644 --- a/luci-app-hermes/luasrc/view/hermes/status.htm +++ b/luci-app-hermes/luasrc/view/hermes/status.htm @@ -1,30 +1,86 @@ -<% -local util = require "luci.util" -local container_status = util.trim(util.exec("/usr/libexec/istorec/hermes.sh status")) -local container_install = (string.len(container_status) > 0) -local container_running = container_status == "running" +<%# + luci-app-hermes — Status Panel (18.06 compat) -%> -
- -
- <% if container_running then %> - - <% else %> - - <% end %> -
+ + + +
+
🤖 Hermes Agent Service Status
+ + + + + + + + + + +
<%:Status%>Loading...
<%:Gateway%>-
<%:Config PTY%>-
Python-
Hermes-
<%:Plugin%>-
<%:Model%>-
<%:Memory%>-
<%:Uptime%>-
-<% -if container_running then - local port = util.trim(util.exec("/usr/libexec/istorec/hermes.sh port")) - if port == "" then - port = "8787" - end --%> -
- -
- -
+ +
+ + +
-<% end %> + + diff --git a/luci-app-hermes/luasrc/view/hermes/terminal.htm b/luci-app-hermes/luasrc/view/hermes/terminal.htm new file mode 100644 index 000000000..99e16d49a --- /dev/null +++ b/luci-app-hermes/luasrc/view/hermes/terminal.htm @@ -0,0 +1,62 @@ +<%# + luci-app-hermes — Config Terminal (18.06 compat) +-%> +<%+header%> +<% +local uci = require "luci.model.uci".cursor() +local pty_port = uci:get("hermes", "main", "pty_port") or "3001" +%> + + +
+

⌨️ <%:Config Terminal%>

+

<%:Interactive terminal for Hermes Agent configuration — AI models, health check, and system management.%>

+
+ +
+
⏳ <%:Connecting to config terminal...%>
+
+ + +<%+footer%> diff --git a/luci-app-hermes/po/zh-cn b/luci-app-hermes/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-hermes/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-hermes/po/zh-cn/hermes.po b/luci-app-hermes/po/zh-cn/hermes.po deleted file mode 100644 index 2fbe0b8a4..000000000 --- a/luci-app-hermes/po/zh-cn/hermes.po +++ /dev/null @@ -1,44 +0,0 @@ -msgid "" -msgstr "Content-Type: text/plain; charset=UTF-8" - -msgid "Official website:" -msgstr "官方网站:" - -msgid "Hermes" -msgstr "Hermes 消息服务" - -msgid "The self-improving AI agent that runs on your server. Layered memory that accumulates across sessions, a cron scheduler that fires while you're offline, and a skills system that saves reusable procedures automatically." -msgstr "运行在您的服务器上的自我改进型人工智能代理。它采用跨会话累积的分层内存、在您离线时运行的定时任务调度程序,以及自动保存可重用程序的技能系统。" - -msgid "Port" -msgstr "端口" - -msgid "Data path" -msgstr "数据路径" - -msgid "Workspace path" -msgstr "工作目录路径" - -msgid "Service Status" -msgstr "服务状态" - -msgid "Hermes status:" -msgstr "Hermes 状态信息如下:" - -msgid "Setup" -msgstr "安装配置" - -msgid "The following parameters will only take effect during installation or upgrade:" -msgstr "以下参数只在安装或者升级时才会生效:" - -msgid "Status" -msgstr "状态" - -msgid "Hermes is running" -msgstr "Hermes 运行中" - -msgid "Hermes is not running" -msgstr "Hermes 未运行" - -msgid "Open Hermes" -msgstr "打开 Hermes" diff --git a/luci-app-hermes/po/zh_Hans b/luci-app-hermes/po/zh_Hans deleted file mode 120000 index 41451e4a1..000000000 --- a/luci-app-hermes/po/zh_Hans +++ /dev/null @@ -1 +0,0 @@ -zh-cn \ No newline at end of file diff --git a/luci-app-hermes/po/zh_Hans/hermes.po b/luci-app-hermes/po/zh_Hans/hermes.po new file mode 100644 index 000000000..607c599d6 --- /dev/null +++ b/luci-app-hermes/po/zh_Hans/hermes.po @@ -0,0 +1,257 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: luci-app-hermes\n" +"Language: zh_Hans\n" + +msgid "Active Model" +msgstr "活跃模型" + +msgid "All Interfaces" +msgstr "所有接口" + +msgid "API Endpoint" +msgstr "API 端点" + +msgid "API Key" +msgstr "API 密钥" + +msgid "API Settings" +msgstr "API 设置" + +msgid "Already up to date" +msgstr "已是最新版本" + +msgid "Basic Settings" +msgstr "基本设置" + +msgid "Cancel" +msgstr "取消" + +msgid "Check Update" +msgstr "检查更新" + +msgid "Checking..." +msgstr "检查中..." + +msgid "Complete" +msgstr "完成" + +msgid "Config PTY" +msgstr "配置终端" + +msgid "Config Terminal" +msgstr "配置终端" + +msgid "Config terminal is not running." +msgstr "配置终端未运行。" + +msgid "Confirm Uninstall" +msgstr "确认卸载" + +msgid "Connecting..." +msgstr "正在连接..." + +msgid "Connecting to config terminal..." +msgstr "正在连接配置终端..." + +msgid "Default: 3000" +msgstr "默认: 3000" + +msgid "Default: 3001" +msgstr "默认: 3001" + +msgid "Disabled" +msgstr "已禁用" + +msgid "Enable Service" +msgstr "启用服务" + +msgid "Environment uninstalled successfully" +msgstr "环境已成功卸载" + +msgid "Error: " +msgstr "错误:" + +msgid "Executing: " +msgstr "正在执行:" + +msgid "Failed" +msgstr "失败" + +msgid "Failed to check status" +msgstr "检查状态失败" + +msgid "Gateway Port" +msgstr "网关端口" + +msgid "Gateway is not running. Please start the service first." +msgstr "网关未运行。请先启动服务。" + +msgid "Hermes" +msgstr "Hermes" + +msgid "Hermes Agent" +msgstr "Hermes Agent" + +msgid "Hermes Agent Web UI — configure AI models and interact with the AI agent." +msgstr "Hermes Agent Web UI — 配置 AI 模型并与 AI 代理交互。" + +msgid "Install" +msgstr "安装" + +msgid "Install Environment" +msgstr "安装环境" + +msgid "Installation failed. Check the log above for details." +msgstr "安装失败,请查看上方日志了解详情。" + +msgid "Installation successful!" +msgstr "安装成功!" + +msgid "Installing..." +msgstr "安装中..." + +msgid "Interactive terminal for Hermes Agent configuration — AI models, health check, and system management." +msgstr "Hermes Agent 交互式终端 — AI 模型配置、健康检查和系统管理。" + +msgid "Listen Interface" +msgstr "监听接口" + +msgid "Log" +msgstr "日志" + +msgid "Memory" +msgstr "内存" + +msgid "Model" +msgstr "模型" + +msgid "More" +msgstr "更多" + +msgid "Not installed" +msgstr "未安装" + +msgid "Overview" +msgstr "概览" + +msgid "PID" +msgstr "进程号" + +msgid "Plugin" +msgstr "插件" + +msgid "Please start the Hermes service first." +msgstr "请先启动 Hermes 服务。" + +msgid "Please start the service first." +msgstr "请先启动服务。" + +msgid "Python" +msgstr "Python" + +msgid "Python AI Agent Gateway for OpenWrt" +msgstr "OpenWrt 路由器 Python AI 代理网关" + +msgid "Python-based AI Agent Gateway for OpenWrt routers." +msgstr "基于 Python 的 OpenWrt 路由器 AI 代理网关。" + +msgid "PTY Port" +msgstr "终端端口" + +msgid "Refresh" +msgstr "刷新" + +msgid "Refresh the page to see updated status." +msgstr "刷新页面查看最新状态。" + +msgid "Requires internet connection and sufficient disk space (~100MB)." +msgstr "需要互联网连接和足够的磁盘空间(约100MB)。" + +msgid "Reset" +msgstr "重置" + +msgid "Restart" +msgstr "重启" + +msgid "Running" +msgstr "运行中" + +msgid "Save & Apply" +msgstr "保存并应用" + +msgid "Service Control" +msgstr "服务控制" + +msgid "Service Status" +msgstr "服务状态" + +msgid "Settings" +msgstr "设置" + +msgid "Settings saved successfully" +msgstr "设置已保存" + +msgid "Start" +msgstr "启动" + +msgid "Starting" +msgstr "启动中" + +msgid "Starting installation" +msgstr "开始安装" + +msgid "Stop" +msgstr "停止" + +msgid "Stopped" +msgstr "已停止" + +msgid "System Information" +msgstr "系统信息" + +msgid "This action cannot be undone." +msgstr "此操作不可撤销。" + +msgid "This will remove Python, Hermes Agent, and all related data." +msgstr "这将移除 Python、Hermes Agent 和所有相关数据。" + +msgid "This will install Python 3 and Hermes Agent via pip." +msgstr "这将通过 pip 安装 Python 3 和 Hermes Agent。" + +msgid "Uninstall" +msgstr "卸载" + +msgid "Uninstalling..." +msgstr "正在卸载..." + +msgid "Unknown" +msgstr "未知" + +msgid "Update Available" +msgstr "有可用更新" + +msgid "Upgrade" +msgstr "升级" + +msgid "Upgrade complete" +msgstr "升级完成" + +msgid "Upgrade successful!" +msgstr "升级成功!" + +msgid "Upgrading to " +msgstr "正在升级到 " + +msgid "Upgrading..." +msgstr "升级中..." + +msgid "Uptime" +msgstr "运行时间" + +msgid "Web Console" +msgstr "Web 控制台" + +msgid "Web console is not available." +msgstr "Web 控制台不可用。" diff --git a/luci-app-hermes/root/etc/config/hermes b/luci-app-hermes/root/etc/config/hermes index 3472a9aad..87a14ad2b 100644 --- a/luci-app-hermes/root/etc/config/hermes +++ b/luci-app-hermes/root/etc/config/hermes @@ -1,4 +1,10 @@ -config main - option 'port' '8787' - option 'data_path' '' - option 'workspace_path' '' +config hermes 'main' + option enabled '0' + option port '3000' + option bind 'lan' + option api_endpoint '' + option api_key '' + option model '' + option pty_port '3001' + option token '' + option pty_token '' diff --git a/luci-app-hermes/root/etc/init.d/hermes b/luci-app-hermes/root/etc/init.d/hermes new file mode 100755 index 000000000..21110aef6 --- /dev/null +++ b/luci-app-hermes/root/etc/init.d/hermes @@ -0,0 +1,419 @@ +#!/bin/sh /etc/rc.common +# luci-app-hermes — procd init script + +USE_PROCD=1 +START=99 +STOP=10 + +EXTRA_COMMANDS="setup status_service restart_gateway" +EXTRA_HELP=" setup Install Python and Hermes Agent + status_service Show service status + restart_gateway Restart only Gateway instance (does not affect Web PTY)" + +HERMES_BASE="/opt/hermes" +HERMES_VENV="${HERMES_BASE}/venv" +HERMES_DATA="${HERMES_BASE}/data" +HERMES_CONFIG="${HERMES_DATA}/config.yaml" +HERMES_BIN="${HERMES_VENV}/bin/hermes" +PYTHON_BIN="${HERMES_VENV}/bin/python3" + +# ── OverlayFS compatibility fix ── +# Docker bind mount (/overlay/upper/opt/docker) makes /opt unwritable +# Solution: bind mount upper layer /opt to merged view /opt +_hermes_fix_opt() { + mkdir -p /opt/hermes/.probe 2>/dev/null && { rmdir /opt/hermes/.probe 2>/dev/null; return 0; } + if [ -d /overlay/upper/opt ]; then + mkdir -p /overlay/upper/opt/hermes 2>/dev/null + mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0 + fi + return 1 +} +_hermes_fix_opt + +# ── Port listening check ── +_ensure_port_free() { + local p="$1" + local i=0 max_wait=3 + + # Check if port is already in use + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + else + netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + fi + + # Port is occupied, try to clean up + for hermes_pid in $(pgrep -f "hermes.*gateway" 2>/dev/null); do + kill "$hermes_pid" 2>/dev/null + done + + while [ $i -lt $max_wait ]; do + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + else + netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + fi + sleep 1 + i=$((i + 1)) + done + + # Last resort: SIGKILL + local port_pid="" + if command -v ss >/dev/null 2>&1; then + port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1) + else + port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1) + fi + [ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1 + return 0 +} + +port_listening() { + local p="$1" + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${p} " + else + netstat -tulnp 2>/dev/null | grep -q ":${p} " + fi +} + +sync_uci_to_yaml() { + # Sync UCI config to /root/.hermes/config.yaml + local port bind api_endpoint api_key model + port=$(uci -q get hermes.main.port || echo "3000") + bind=$(uci -q get hermes.main.bind || echo "lan") + api_endpoint=$(uci -q get hermes.main.api_endpoint || echo "https://api.boos.lat/v1") + api_key=$(uci -q get hermes.main.api_key || echo "") + model=$(uci -q get hermes.main.model || echo "opus4.6") + + # Map UCI bind to listen address + local listen_host="127.0.0.1" + case "$bind" in + lan) listen_host="0.0.0.0" ;; + loopback) listen_host="127.0.0.1" ;; + all) listen_host="0.0.0.0" ;; + *) listen_host="0.0.0.0" ;; + esac + + # Ensure config directory exists + mkdir -p /root/.hermes 2>/dev/null + mkdir -p "$(dirname "$HERMES_CONFIG")" 2>/dev/null + + # Generate token if not set + local token + token=$(uci -q get hermes.main.token || echo "") + if [ -z "$token" ]; then + token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "hermes_$(date +%s)") + uci set hermes.main.token="$token" + uci commit hermes 2>/dev/null + fi + + # Write config.yaml using shell (no Python dependency at this point) + cat > /root/.hermes/config.yaml << YAMLEOF +# Hermes Agent configuration — auto-generated by luci-app-hermes +# UCI sync: $(date '+%Y-%m-%d %H:%M:%S') + +gateway: + port: ${port} + bind: ${listen_host} + auth: + mode: token + token: ${token} + +api: + endpoint: "${api_endpoint}" + key: "${api_key}" + +model: + primary: "${model}" + +tools: + profile: coding + +update: + checkOnStart: false +YAMLEOF + + # Also sync to HERMES_DATA if different + if [ -n "$HERMES_CONFIG" ] && [ "$HERMES_CONFIG" != "/root/.hermes/config.yaml" ]; then + mkdir -p "$(dirname "$HERMES_CONFIG")" 2>/dev/null + cp /root/.hermes/config.yaml "$HERMES_CONFIG" 2>/dev/null || true + fi + + chmod 600 /root/.hermes/config.yaml 2>/dev/null || true +} + +start_service() { + local enabled port bind pty_port api_endpoint api_key model + config_load hermes + config_get enabled main enabled "0" + config_get port main port "3000" + config_get bind main bind "lan" + config_get pty_port main pty_port "3001" + config_get api_endpoint main api_endpoint "https://api.boos.lat/v1" + config_get api_key main api_key "" + config_get model main model "opus4.6" + + [ "$enabled" = "1" ] || { + echo "hermes is disabled. Set enabled to 1 in /etc/config/hermes" + return 0 + } + + # Check Python + if [ ! -x "$PYTHON_BIN" ]; then + echo "Python not found: $PYTHON_BIN" + echo "Please run: hermes-env setup" + return 1 + fi + + # Check hermes binary + if [ ! -x "$HERMES_BIN" ]; then + echo "Hermes not installed. Please run: hermes-env setup" + return 1 + fi + + # Sync UCI to config.yaml + sync_uci_to_yaml + + # Ensure gateway port is not occupied by stale processes + _ensure_port_free "$port" + + # Calculate memory limit based on system RAM + local mem_limit="" + local total_kb + total_kb=$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo "0") + if [ "$total_kb" -ge 3000000 ] 2>/dev/null; then + mem_limit="512m" + elif [ "$total_kb" -ge 1500000 ] 2>/dev/null; then + mem_limit="256m" + elif [ "$total_kb" -ge 700000 ] 2>/dev/null; then + mem_limit="128m" + else + mem_limit="64m" + fi + + # Start Hermes Gateway (main service, foreground) + procd_open_instance "gateway" + procd_set_param command "$HERMES_BIN" gateway run \ + --port "$port" --bind "$bind" + procd_set_param env \ + HOME="/root" \ + HERMES_HOME="/root/.hermes" \ + HERMES_CONFIG="/root/.hermes/config.yaml" \ + PATH="${HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \ + PYTHONPATH="${HERMES_VENV}/lib/python3*/site-packages" \ + HERMES_API_ENDPOINT="$api_endpoint" \ + HERMES_API_KEY="$api_key" \ + HERMES_MODEL="$model" + procd_set_param respawn 3600 10 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/hermes.pid + procd_close_instance + + # Start Web PTY config terminal (auxiliary service) + local pty_token + pty_token=$(uci -q get hermes.main.pty_token || echo "") + if [ -z "$pty_token" ]; then + pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)") + uci set hermes.main.pty_token="$pty_token" + uci commit hermes 2>/dev/null + fi + + procd_open_instance "pty" + procd_set_param command "$PYTHON_BIN" /usr/share/hermes/web-pty.py + procd_set_param env \ + HERMES_PTY_PORT="$pty_port" \ + HERMES_PTY_TOKEN="$pty_token" \ + HERMES_VENV="$HERMES_VENV" \ + HERMES_DATA="$HERMES_DATA" \ + HOME="/root" \ + PATH="${HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + procd_set_param respawn 3600 5 5 + procd_set_param stdout 1 + procd_set_param stderr 1 + procd_set_param pidfile /var/run/hermes-pty.pid + procd_close_instance +} + +_ensure_port_free() { + local p="$1" + local i=0 max_wait=3 + + # Check if port is already in use + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + else + netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + fi + + # Port is occupied, try to clean up + for hermes_pid in $(pgrep -f "hermes.*gateway" 2>/dev/null); do + kill "$hermes_pid" 2>/dev/null + done + + while [ $i -lt $max_wait ]; do + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + else + netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0 + fi + sleep 1 + i=$((i + 1)) + done + + # Last resort: SIGKILL + local port_pid="" + if command -v ss >/dev/null 2>&1; then + port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1) + else + port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1) + fi + [ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1 + return 0 +} + +stop_service() { + local port + port=$(uci -q get hermes.main.port || echo "3000") + + # Get all related PIDs + local gw_pids="" + gw_pids=$(pgrep -f "hermes.*gateway" 2>/dev/null) + gw_pids="$gw_pids $(pgrep -f "hermes.*gateway.*run" 2>/dev/null)" + + # Send SIGTERM to all processes + for pid in $gw_pids; do + [ -n "$pid" ] && kill "$pid" 2>/dev/null + done + + # Wait for port to be released + local wait_count=0 + local max_wait=4 + while [ $wait_count -lt $max_wait ]; do + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${port} " || break + else + netstat -tulnp 2>/dev/null | grep -q ":${port} " || break + fi + sleep 1 + wait_count=$((wait_count + 1)) + done + + # Force kill if still running + if [ $wait_count -ge $max_wait ]; then + local port_pid="" + if command -v ss >/dev/null 2>&1; then + port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1) + else + port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1) + fi + if [ -n "$port_pid" ]; then + kill -9 "$port_pid" 2>/dev/null + sleep 1 + fi + fi +} + +service_triggers() { + procd_add_reload_trigger "hermes" +} + +reload_service() { + stop + sleep 1 + start +} + +setup() { + echo "Calling hermes-env setup..." + /usr/bin/hermes-env setup +} + +restart_gateway() { + # Restart only the Gateway process, does not affect PTY + local port + port=$(uci -q get hermes.main.port || echo "3000") + + # Get gateway PID + local port_pid="" gw_pid="" + if command -v ss >/dev/null 2>&1; then + port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1) + else + port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1) + fi + gw_pid=$(ubus call service list '{"name":"hermes"}' 2>/dev/null | \ + jsonfilter -e '$.hermes.instances.gateway.pid' 2>/dev/null) || true + + # Send SIGTERM + [ -n "$port_pid" ] && kill "$port_pid" 2>/dev/null + [ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null && kill "$gw_pid" 2>/dev/null + + # Kill any remaining hermes gateway processes + for pid in $(pgrep -f "hermes.*gateway" 2>/dev/null) $(pgrep -f "hermes.*gateway.*run" 2>/dev/null); do + kill "$pid" 2>/dev/null + done + + # Wait for port to be released + local wait_count=0 + while [ $wait_count -lt 3 ]; do + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${port} " || break + else + netstat -tulnp 2>/dev/null | grep -q ":${port} " || break + fi + sleep 1 + wait_count=$((wait_count + 1)) + done + + # Force kill if still running + if [ $wait_count -ge 3 ]; then + [ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null + sleep 1 + fi + + # If no procd gateway, restart the service + if [ -z "$gw_pid" ]; then + /etc/init.d/hermes start >/dev/null 2>&1 + fi +} + +status_service() { + local port pty_port + port=$(uci -q get hermes.main.port || echo "3000") + pty_port=$(uci -q get hermes.main.pty_port || echo "3001") + + echo "=== Hermes Agent Service Status ===" + + # Python + if [ -x "$PYTHON_BIN" ]; then + echo "Python: $($PYTHON_BIN --version 2>/dev/null)" + else + echo "Python: Not installed" + fi + + # Hermes Agent + if [ -x "$HERMES_BIN" ]; then + echo "Hermes: $($HERMES_BIN --version 2>/dev/null | head -1)" + else + echo "Hermes: Not installed" + fi + + # Gateway + if port_listening "$port"; then + echo "Gateway: ✅ Running (:$port)" + else + echo "Gateway: ⏹ Stopped" + fi + + # PTY + if port_listening "$pty_port"; then + echo "Config PTY: ✅ Running (:$pty_port)" + else + echo "Config PTY: ⏹ Stopped" + fi + + # Config + echo "Config: $HERMES_CONFIG" + echo "Data Dir: $HERMES_DATA" +} diff --git a/luci-app-hermes/root/etc/uci-defaults/99-hermes b/luci-app-hermes/root/etc/uci-defaults/99-hermes new file mode 100755 index 000000000..8f4b60c6b --- /dev/null +++ b/luci-app-hermes/root/etc/uci-defaults/99-hermes @@ -0,0 +1,34 @@ +#!/bin/sh +# luci-app-hermes — First-boot setup (uci-defaults) +# This runs once after package installation + +# Set default values if not already set +uci -q set hermes.main.enabled='0' 2>/dev/null +uci -q set hermes.main.port='3000' 2>/dev/null +uci -q set hermes.main.bind='lan' 2>/dev/null +uci -q set hermes.main.api_endpoint='https://api.boos.lat/v1' 2>/dev/null +uci -q set hermes.main.api_key='' 2>/dev/null +uci -q set hermes.main.model='opus4.6' 2>/dev/null +uci -q set hermes.main.pty_port='3001' 2>/dev/null + +# Generate auth token if not set +token=$(uci -q get hermes.main.token || echo "") +if [ -z "$token" ]; then + token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "hermes_$(date +%s)") + uci -q set hermes.main.token="$token" +fi + +# Generate PTY token if not set +pty_token=$(uci -q get hermes.main.pty_token || echo "") +if [ -z "$pty_token" ]; then + pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)") + uci -q set hermes.main.pty_token="$pty_token" +fi + +uci -q commit hermes 2>/dev/null + +# Create required directories +mkdir -p /root/.hermes 2>/dev/null +mkdir -p /opt/hermes/data 2>/dev/null + +exit 0 diff --git a/luci-app-hermes/root/usr/bin/hermes-env b/luci-app-hermes/root/usr/bin/hermes-env new file mode 100755 index 000000000..4b1f141c0 --- /dev/null +++ b/luci-app-hermes/root/usr/bin/hermes-env @@ -0,0 +1,447 @@ +#!/bin/sh +# ============================================================================ +# hermes-env — Hermes Agent environment installation management script +# Usage: hermes-env setup | check | upgrade +# Environment variables: HERMES_VERSION — specify version to install +# ============================================================================ + +# ── Version and path constants ───────────────────────────────────────────── +HERMES_BASE="/opt/hermes" +HERMES_VENV="${HERMES_BASE}/venv" +HERMES_DATA="${HERMES_BASE}/data" +HERMES_BIN="${HERMES_VENV}/bin/hermes" +PYTHON_BIN="${HERMES_VENV}/bin/python3" +HERMES_CONFIG="/root/.hermes/config.yaml" + +# PyPI mirrors +HERMES_PYPI_CN="https://mirrors.aliyun.com/pypi/simple/" +HERMES_PYPI_OFFICIAL="https://pypi.org/simple/" +HERMES_PYPI_TSINGHUA="https://pypi.tuna.tsinghua.edu.cn/simple/" + +# Default API endpoint +HERMES_DEFAULT_API="https://api.boos.lat/v1" +HERMES_DEFAULT_MODEL="opus4.6" + +export PATH="${HERMES_VENV}/bin:$PATH" + +# ── Logging tools ────────────────────────────────────────────────────────── +log_ok() { echo " [✓] $*"; } +log_warn() { echo " [!] $*"; } +log_err() { echo " [✗] $*"; } +log_step() { echo ""; echo "=== $* ==="; } + +# ── OverlayFS fix (iStoreOS Docker /opt conflict) ───────────────────────── +_fix_opt() { + mkdir -p /opt/hermes/.probe 2>/dev/null && { rmdir /opt/hermes/.probe 2>/dev/null; return 0; } + [ -d /overlay/upper/opt ] && mkdir -p /overlay/upper/opt/hermes 2>/dev/null && \ + mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0 + return 1 +} +_fix_opt + +# ── Basic tools ──────────────────────────────────────────────────────────── +ensure_dir() { [ -d "$1" ] && return 0; mkdir -p "$1" 2>/dev/null || { log_err "Cannot create directory: $1"; return 1; }; } + +# Check available disk space +_df_avail() { + local p="$1" + while [ -n "$p" ] && [ "$p" != "/" ]; do + [ -e "$p" ] && break + p="${p%/*}" + done + [ -z "$p" ] && p="/" + df -m "$p" 2>/dev/null | tail -1 | awk '{print $(NF-2)}' +} + +check_space() { + local path="$1" need_mb="$2" label="$3" + local avail + avail=$(_df_avail "$path") + [ -z "$avail" ] && return 0 + if [ "$avail" -lt "$need_mb" ]; then + log_warn "${label} available space insufficient: ${avail}MB < ${need_mb}MB recommended" + else + log_ok "${label} available space: ${avail}MB" + fi +} + +# Download: wget first, curl fallback +_download() { + local url="$1" out="$2" + if command -v wget >/dev/null 2>&1; then + wget -q --no-check-certificate -O "$out" "$url" 2>/dev/null + elif command -v curl >/dev/null 2>&1; then + curl -fsSL --connect-timeout 15 --max-time 120 -o "$out" "$url" 2>/dev/null + else + log_err "No download tool available (wget/curl)"; return 1 + fi +} + +# Try multiple mirrors, return first success +_download_first() { + local out="$1"; shift + for url in "$@"; do + [ -z "$url" ] && continue + if _download "$url" "$out"; then + local sz; sz=$(wc -c < "$out" 2>/dev/null || echo 0) + [ "$sz" -gt 1000 ] && return 0 + fi + rm -f "$out" + done + return 1 +} + +# ── Architecture detection ───────────────────────────────────────────────── +detect_arch() { + case "$(uname -m)" in + x86_64) echo "x86_64" ;; + aarch64) echo "aarch64" ;; + armv7l) echo "armv7l" ;; + armv6l) echo "armv6l" ;; + *) log_err "Unsupported architecture: $(uname -m)"; exit 1 ;; + esac +} + +detect_libc() { + [ -f /etc/openwrt_release ] && echo "musl" && return + for f in /lib/ld-musl-*.so.1; do [ -f "$f" ] && echo "musl" && return; done + echo "glibc" +} + +# ── Install Python 3 ────────────────────────────────────────────────────── +install_python() { + # Check if system python3 is available + if command -v python3 >/dev/null 2>&1; then + local ver + ver=$(python3 --version 2>/dev/null | awk '{print $2}') + log_ok "System Python3 v${ver} found" + # Check if it's recent enough (3.8+) + local major minor + major=$(echo "$ver" | cut -d. -f1) + minor=$(echo "$ver" | cut -d. -f2) + if [ "$major" -ge 3 ] && [ "$minor" -ge 8 ] 2>/dev/null; then + log_ok "Python3 v${ver} meets requirements (>= 3.8)" + return 0 + fi + log_warn "Python3 v${ver} is too old, need >= 3.8" + fi + + log_step "Installing Python 3" + + # Try opkg first + opkg update >/dev/null 2>&1 || true + opkg install python3 python3-pip python3-venv 2>/dev/null | tail -5 || true + + if command -v python3 >/dev/null 2>&1; then + log_ok "Python3 installed via opkg: $(python3 --version 2>/dev/null)" + return 0 + fi + + # Fallback: try to install minimal python3 + opkg install python3-base python3-light 2>/dev/null | tail -5 || true + + if command -v python3 >/dev/null 2>&1; then + log_ok "Python3 (minimal) installed: $(python3 --version 2>/dev/null)" + # Also need pip + opkg install python3-pip 2>/dev/null || true + return 0 + fi + + log_err "Python3 installation failed" + log_err "Please install manually: opkg install python3 python3-pip python3-venv" + return 1 +} + +# ── Create virtual environment ───────────────────────────────────────────── +create_venv() { + log_step "Creating Python virtual environment" + + ensure_dir "$HERMES_BASE" || exit 1 + + # Remove old venv if exists + if [ -d "$HERMES_VENV" ]; then + log_warn "Removing old virtual environment..." + rm -rf "$HERMES_VENV" + fi + + # Create venv using system python3 + local sys_python + sys_python=$(command -v python3 2>/dev/null) + if [ -z "$sys_python" ]; then + log_err "python3 not found in PATH" + return 1 + fi + + "$sys_python" -m venv "$HERMES_VENV" 2>/dev/null + if [ ! -x "$PYTHON_BIN" ]; then + # Try without -m venv (older python) + log_warn "python3 -m venv failed, trying virtualenv..." + pip3 install virtualenv 2>/dev/null || true + "$sys_python" -m virtualenv "$HERMES_VENV" 2>/dev/null + fi + + if [ ! -x "$PYTHON_BIN" ]; then + log_err "Failed to create virtual environment" + return 1 + fi + + log_ok "Virtual environment created at $HERMES_VENV" + + # Upgrade pip + log_ok "Upgrading pip..." + "$PYTHON_BIN" -m pip install --upgrade pip 2>/dev/null | tail -2 || true + + log_ok "Python: $("$PYTHON_BIN" --version 2>/dev/null)" +} + +# ── Install Hermes Agent ─────────────────────────────────────────────────── +install_hermes() { + log_step "Installing Hermes Agent" + + # Check if already installed + if [ -x "$HERMES_BIN" ]; then + local cur + cur=$("$HERMES_BIN" --version 2>/dev/null | head -1 | tr -d '[:space:]') + log_ok "Hermes Agent v${cur} already installed" + echo " To reinstall: hermes-env setup --force" + return 0 + fi + + ensure_dir "$HERMES_VENV" || return 1 + + # Try PyPI mirrors in order + local install_ok=false + + for mirror in "$HERMES_PYPI_CN" "$HERMES_PYPI_TSINGHUA" "$HERMES_PYPI_OFFICIAL"; do + log_warn "Trying: ${mirror}" + if "$PYTHON_BIN" -m pip install hermes-agent \ + -i "$mirror" \ + --trusted-host "$(echo "$mirror" | sed 's|https://||;s|/.*||')" \ + 2>&1 | tail -10; then + install_ok=true + break + fi + done + + if [ "$install_ok" = "false" ] || [ ! -x "$HERMES_BIN" ]; then + # Try without mirror flags + log_warn "Mirror install failed, trying default PyPI..." + "$PYTHON_BIN" -m pip install hermes-agent 2>&1 | tail -10 || true + [ -x "$HERMES_BIN" ] && install_ok=true + fi + + if [ "$install_ok" = "false" ]; then + log_err "Hermes Agent installation failed" + log_err "Please check network connection and try again" + return 1 + fi + + log_ok "Hermes Agent installed: $("$HERMES_BIN" --version 2>/dev/null | head -1)" +} + +# ── Initialize Hermes configuration ──────────────────────────────────────── +init_hermes() { + log_step "Initializing Hermes configuration" + + mkdir -p /root/.hermes 2>/dev/null + + # Generate auth token + local token + token=$(uci -q get hermes.main.token 2>/dev/null || echo "") + if [ -z "$token" ]; then + token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "hermes_$(date +%s)") + uci set hermes.main.token="$token" 2>/dev/null || true + uci commit hermes 2>/dev/null || true + fi + + # Generate PTY token + local pty_token + pty_token=$(uci -q get hermes.main.pty_token 2>/dev/null || echo "") + if [ -z "$pty_token" ]; then + pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)") + uci set hermes.main.pty_token="$pty_token" 2>/dev/null || true + uci commit hermes 2>/dev/null || true + fi + + # Write default config.yaml + local port bind api_endpoint api_key model + port=$(uci -q get hermes.main.port || echo "3000") + bind=$(uci -q get hermes.main.bind || echo "lan") + api_endpoint=$(uci -q get hermes.main.api_endpoint || echo "$HERMES_DEFAULT_API") + api_key=$(uci -q get hermes.main.api_key || echo "") + model=$(uci -q get hermes.main.model || echo "$HERMES_DEFAULT_MODEL") + + local listen_host="0.0.0.0" + [ "$bind" = "loopback" ] && listen_host="127.0.0.1" + + cat > /root/.hermes/config.yaml << YAMLEOF +# Hermes Agent configuration +# Initialized by luci-app-hermes on $(date '+%Y-%m-%d %H:%M:%S') + +gateway: + port: ${port} + bind: ${listen_host} + auth: + mode: token + token: ${token} + +api: + endpoint: "${api_endpoint}" + key: "${api_key}" + +model: + primary: "${model}" + +tools: + profile: coding + +update: + checkOnStart: false +YAMLEOF + + chmod 600 /root/.hermes/config.yaml 2>/dev/null || true + log_ok "Configuration written to /root/.hermes/config.yaml" +} + +# ── Command: setup ───────────────────────────────────────────────────────── +cmd_setup() { + check_space /opt 100 "Install directory (/opt)" + check_space /tmp 100 "Temp directory (/tmp)" + + # Already installed? + if [ -x "$HERMES_BIN" ]; then + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ [!] Hermes Agent already installed ║" + echo "║ To reinstall, run: hermes-env setup --force ║" + echo "╚══════════════════════════════════════════════════════════════╝" + echo " Hermes: $("$HERMES_BIN" --version 2>/dev/null | head -1)" + exit 0 + fi + + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ Hermes Agent Environment Setup ║" + echo "╚══════════════════════════════════════════════════════════════╝" + echo " Architecture: $(uname -m)" + echo " Libc: $(detect_libc)" + echo "" + + install_python || exit 1 + create_venv || exit 1 + install_hermes || exit 1 + init_hermes || exit 1 + + echo "" + echo "╔══════════════════════════════════════════════════════════════╗" + echo "║ Setup Complete! ║" + echo "╚══════════════════════════════════════════════════════════════╝" + echo " Hermes: $("$HERMES_BIN" --version 2>/dev/null | head -1)" + echo " Python: $("$PYTHON_BIN" --version 2>/dev/null)" + echo " Config: /root/.hermes/config.yaml" + echo "" + echo " Next steps:" + echo " 1. Edit /etc/config/hermes to set your API key" + echo " 2. Run: /etc/init.d/hermes enable" + echo " 3. Run: /etc/init.d/hermes start" + echo "" + echo " Or use the LuCI web interface to configure and start." +} + +# ── Command: check ───────────────────────────────────────────────────────── +cmd_check() { + echo "=== Hermes Environment Check ===" + + if [ -x "$PYTHON_BIN" ]; then + echo "Python: $("$PYTHON_BIN" --version 2>/dev/null)" + else + echo "Python: Not installed (virtual env)" + fi + + if command -v python3 >/dev/null 2>&1; then + echo "System Py: $(python3 --version 2>/dev/null)" + else + echo "System Py: Not found" + fi + + if [ -x "$HERMES_BIN" ]; then + echo "Hermes: $("$HERMES_BIN" --version 2>/dev/null | head -1)" + else + echo "Hermes: Not installed" + fi + + echo "VENV: $HERMES_VENV" + echo "Config: $HERMES_CONFIG" + + if [ -f "$HERMES_CONFIG" ]; then + echo "Config OK: Yes" + else + echo "Config OK: No (will be created on start)" + fi +} + +# ── Command: upgrade ─────────────────────────────────────────────────────── +cmd_upgrade() { + if [ ! -x "$HERMES_BIN" ]; then + log_err "Hermes Agent not installed. Run: hermes-env setup" + exit 1 + fi + + log_step "Upgrading Hermes Agent" + + local upgrade_ok=false + + for mirror in "$HERMES_PYPI_CN" "$HERMES_PYPI_TSINGHUA" "$HERMES_PYPI_OFFICIAL"; do + log_warn "Trying: ${mirror}" + if "$PYTHON_BIN" -m pip install --upgrade hermes-agent \ + -i "$mirror" \ + --trusted-host "$(echo "$mirror" | sed 's|https://||;s|/.*||')" \ + 2>&1 | tail -10; then + upgrade_ok=true + break + fi + done + + if [ "$upgrade_ok" = "false" ]; then + log_warn "Mirror install failed, trying default PyPI..." + "$PYTHON_BIN" -m pip install --upgrade hermes-agent 2>&1 | tail -10 || true + [ -x "$HERMES_BIN" ] && upgrade_ok=true + fi + + if [ "$upgrade_ok" = "true" ] && [ -x "$HERMES_BIN" ]; then + log_ok "Hermes upgraded: $("$HERMES_BIN" --version 2>/dev/null | head -1)" + else + log_err "Upgrade failed" + exit 1 + fi +} + +# ── Command: uninstall ───────────────────────────────────────────────────── +cmd_uninstall() { + log_step "Uninstalling Hermes Agent" + + /etc/init.d/hermes stop 2>/dev/null || true + /etc/init.d/hermes disable 2>/dev/null || true + uci set hermes.main.enabled=0 2>/dev/null || true + uci commit hermes 2>/dev/null || true + + [ -d "$HERMES_BASE" ] && { rm -rf "$HERMES_BASE" 2>/dev/null || true; } + [ -d "/root/.hermes" ] && { rm -rf "/root/.hermes" 2>/dev/null || true; } + + log_ok "Hermes Agent uninstalled" +} + +# ── Main entry ───────────────────────────────────────────────────────────── +case "${1:-setup}" in + setup) cmd_setup ;; + check) cmd_check ;; + upgrade) cmd_upgrade ;; + uninstall) cmd_uninstall ;; + *) + echo "Usage: hermes-env {setup|check|upgrade|uninstall}" + echo "" + echo " setup Install Python + Hermes Agent (default)" + echo " check Check installation status" + echo " upgrade Upgrade Hermes Agent to latest version" + echo " uninstall Remove all Hermes files" + exit 1 + ;; +esac diff --git a/luci-app-hermes/root/usr/libexec/istorec/hermes.sh b/luci-app-hermes/root/usr/libexec/istorec/hermes.sh deleted file mode 100755 index 775a611a3..000000000 --- a/luci-app-hermes/root/usr/libexec/istorec/hermes.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/bin/sh - -ACTION=${1} -shift 1 - -do_install() { - local port=$(uci get hermes.@main[0].port 2>/dev/null | tr -d '\n\r') - local data=$(uci get hermes.@main[0].data_path 2>/dev/null | tr -d '\n\r') - local workspace=$(uci get hermes.@main[0].workspace_path 2>/dev/null | tr -d '\n\r') - - local image_name="linkease/hermes:latest" - echo "docker pull ${image_name}" - docker pull ${image_name} - docker rm -f hermes - - [ -z "$port" ] && port=8787 - - local cmd="docker run --restart=unless-stopped -d" - - [ -n "$data" ] && cmd="$cmd -v \"$data:/data/.hermes\"" - [ -n "$workspace" ] && cmd="$cmd -v \"$workspace:/workspace\"" - - cmd="$cmd -p $port:8787" - - local tz=$(uci get system.@system[0].zonename 2>/dev/null | sed 's/ /_/g') - [ -z "$tz" ] || cmd="$cmd -e TZ=$tz" - - cmd="$cmd --name hermes \"$image_name\"" - - echo "$cmd" - eval "$cmd" -} - -usage() { - echo "usage: $0 sub-command" - echo "where sub-command is one of:" - echo " install Install the hermes" - echo " upgrade Upgrade the hermes" - echo " rm/start/stop/restart Remove/Start/Stop/Restart the hermes" - echo " status Hermes status" - echo " port Hermes port" -} - -case ${ACTION} in - "install") - do_install - ;; - "upgrade") - do_install - ;; - "rm") - docker rm -f hermes - ;; - "start" | "stop" | "restart") - docker ${ACTION} hermes - ;; - "status") - docker ps --all -f 'name=^/hermes$' --format '{{.State}}' - ;; - "port") - docker ps --all -f 'name=^/hermes$' --format '{{.Ports}}' | grep -om1 '0.0.0.0:[0-9]*->8787/tcp' | sed 's/0.0.0.0:\([0-9]*\)->.*/\1/' - ;; - *) - usage - exit 1 - ;; -esac diff --git a/luci-app-hermes/root/usr/share/hermes/luci-helper b/luci-app-hermes/root/usr/share/hermes/luci-helper new file mode 100755 index 000000000..28b103099 --- /dev/null +++ b/luci-app-hermes/root/usr/share/hermes/luci-helper @@ -0,0 +1,210 @@ +#!/bin/sh +# luci-app-hermes — Backend helper for JS view +# Called via ubus file.exec from the LuCI frontend + +. /lib/functions.sh 2>/dev/null || true + +HERMES_VENV="/opt/hermes/venv" +HERMES_BIN="${HERMES_VENV}/bin/hermes" +HERMES_DATA="/opt/hermes/data" +HERMES_CONFIG="/root/.hermes/config.yaml" + +# ── JSON helper: escape string for JSON output ── +json_esc() { + printf '%s' "$1" | sed 's/\\/\\\\/g;s/"/\\"/g;s/\t/\\t/g' | tr '\n' ' ' +} + +# ── Port listening check (ss or netstat) ── +port_listening() { + local p="$1" + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep -q ":${p} " + else + netstat -tulnp 2>/dev/null | grep -q ":${p} " + fi +} + +pid_by_port() { + local p="$1" + if command -v ss >/dev/null 2>&1; then + ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1 + else + netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1 + fi +} + +case "$1" in + +# ═══════════════════════════════════════════ +# STATUS — gather all status info as JSON +# ═══════════════════════════════════════════ +status) + enabled=$(uci -q get hermes.main.enabled || echo "0") + port=$(uci -q get hermes.main.port || echo "3000") + pty_port=$(uci -q get hermes.main.pty_port || echo "3001") + + # Validate ports + case "$port" in *[!0-9]*) port="3000" ;; esac + case "$pty_port" in *[!0-9]*) pty_port="3001" ;; esac + + # Gateway check + gw_running=false + gw_starting=false + port_listening "$port" && gw_running=true + if [ "$gw_running" = "false" ] && [ "$enabled" = "1" ]; then + pgrep -f 'hermes.*gateway' >/dev/null 2>&1 && gw_starting=true + fi + + # PTY check + pty_running=false + port_listening "$pty_port" && pty_running=true + + # Python version + python_ver="" + if [ -x "${HERMES_VENV}/bin/python3" ]; then + python_ver=$("${HERMES_VENV}/bin/python3" --version 2>/dev/null | tr -d '[:space:]') + elif command -v python3 >/dev/null 2>&1; then + python_ver=$(python3 --version 2>/dev/null | tr -d '[:space:]') + fi + + # Hermes version + hermes_ver="" + if [ -x "$HERMES_BIN" ]; then + hermes_ver=$("$HERMES_BIN" --version 2>/dev/null | head -1 | tr -d '[:space:]') + fi + + # Plugin version + plugin_ver="" + [ -f "/usr/share/hermes/VERSION" ] && plugin_ver=$(cat /usr/share/hermes/VERSION 2>/dev/null | tr -d '[:space:]') + + # PID, memory, uptime + pid="" mem_kb=0 uptime_str="" + if [ "$gw_running" = "true" ]; then + pid=$(pid_by_port "$port") + if [ -n "$pid" ] && [ -d "/proc/$pid" ]; then + mem_kb=$(awk '/VmRSS/{print $2}' "/proc/$pid/status" 2>/dev/null) + [ -z "$mem_kb" ] && mem_kb=0 + # Calculate uptime from /proc/$pid directory mtime + start_ts=$(date -r "/proc/$pid" +%s 2>/dev/null) + if [ -n "$start_ts" ]; then + now=$(date +%s) + diff=$((now - start_ts)) + h=$((diff / 3600)) m=$(((diff % 3600) / 60)) s=$((diff % 60)) + if [ $h -gt 0 ]; then uptime_str="${h}h ${m}m" + elif [ $m -gt 0 ]; then uptime_str="${m}m ${s}s" + else uptime_str="${s}s"; fi + fi + fi + fi + + # Active model (from config.yaml) + active_model="" + if [ -f "$HERMES_CONFIG" ]; then + active_model=$(sed -n 's/.*primary: *"\{0,1\}\([^"]*\)"\{0,1\}/\1/p' "$HERMES_CONFIG" 2>/dev/null | head -1) + fi + + cat < /tmp/hermes-setup.log + : > /tmp/hermes-setup.exit + : > /tmp/hermes-setup.pid + + ( /usr/bin/hermes-env setup > /tmp/hermes-setup.log 2>&1 + RC=$? + echo $RC > /tmp/hermes-setup.exit + if [ $RC -eq 0 ]; then + uci set hermes.main.enabled=1 + uci commit hermes + /etc/init.d/hermes enable 2>/dev/null + sleep 1 + /etc/init.d/hermes start >> /tmp/hermes-setup.log 2>&1 + fi + ) & + echo $! > /tmp/hermes-setup.pid + + echo '{"status":"ok","message":"setup started"}' + ;; + +# ═══════════════════════════════════════════ +# SETUP_LOG — poll setup progress +# ═══════════════════════════════════════════ +setup_log) + log="" + [ -f /tmp/hermes-setup.log ] && log=$(cat /tmp/hermes-setup.log 2>/dev/null) + + running=false + if [ -f /tmp/hermes-setup.pid ]; then + pid=$(cat /tmp/hermes-setup.pid 2>/dev/null | tr -d '[:space:]') + [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null && running=true + fi + + exit_code=-1 + if [ "$running" = "false" ] && [ -f /tmp/hermes-setup.exit ]; then + exit_code=$(cat /tmp/hermes-setup.exit 2>/dev/null | tr -d '[:space:]') + [ -z "$exit_code" ] && exit_code=-1 + fi + + state="idle" + if [ "$running" = "true" ]; then state="running" + elif [ "$exit_code" = "0" ]; then state="success" + elif [ "$exit_code" -gt 0 ] 2>/dev/null; then state="failed" + fi + + # Escape log for JSON + log_escaped=$(printf '%s' "$log" | sed 's/\\/\\\\/g;s/"/\\"/g;s/\t/\\t/g' | tr '\n' '\\' | sed 's/\\/\\n/g') + + printf '{"state":"%s","exit_code":%s,"log":"%s"}\n' "$state" "${exit_code}" "$log_escaped" + ;; + +# ═══════════════════════════════════════════ +# GET_TOKEN — return gateway and PTY tokens +# ═══════════════════════════════════════════ +get_token) + token=$(uci -q get hermes.main.token || echo "") + pty_token=$(uci -q get hermes.main.pty_token || echo "") + printf '{"token":"%s","pty_token":"%s"}\n' "$token" "$pty_token" + ;; + +# ═══════════════════════════════════════════ +# UNINSTALL — remove environment +# ═══════════════════════════════════════════ +uninstall) + /etc/init.d/hermes stop >/dev/null 2>&1 + /etc/init.d/hermes disable 2>/dev/null + uci set hermes.main.enabled=0 + uci commit hermes 2>/dev/null + [ -d /opt/hermes ] && { find /opt/hermes -delete 2>/dev/null || true; } + [ -d /root/.hermes ] && { find /root/.hermes -delete 2>/dev/null || true; } + : > /tmp/hermes-setup.log 2>/dev/null + : > /tmp/hermes-setup.pid 2>/dev/null + : > /tmp/hermes-setup.exit 2>/dev/null + echo '{"status":"ok","message":"uninstalled"}' + ;; + +*) + echo '{"status":"error","message":"unknown command"}' + ;; +esac diff --git a/luci-app-hermes/root/usr/share/hermes/ui/index.html b/luci-app-hermes/root/usr/share/hermes/ui/index.html new file mode 100644 index 000000000..a3b60d662 --- /dev/null +++ b/luci-app-hermes/root/usr/share/hermes/ui/index.html @@ -0,0 +1,278 @@ + + + + + +Hermes Agent Terminal + + + + +
+ + ● Connecting... + +
+ +
+
+

🤖 Hermes Agent Terminal

+

Connecting to terminal...

+
+
+ +
+ +
+

⚡ Connection Lost

+

Terminal session ended or connection interrupted

+ +
+ + + + + + + + + diff --git a/luci-app-hermes/root/usr/share/hermes/web-pty.py b/luci-app-hermes/root/usr/share/hermes/web-pty.py new file mode 100755 index 000000000..1612ae8c5 --- /dev/null +++ b/luci-app-hermes/root/usr/share/hermes/web-pty.py @@ -0,0 +1,549 @@ +#!/usr/bin/env python3 +""" +luci-app-hermes — Web PTY Server (Python implementation) +Provides WebSocket-based terminal access for Hermes Agent configuration. +Replaces the Node.js web-pty.js from luci-app-openclaw. +""" + +import http.server +import socketserver +import json +import os +import sys +import signal +import struct +import hashlib +import base64 +import subprocess +import threading +import time +import select +import pty +import fcntl +import termios +import urllib.parse + +# ── Configuration ── +PORT = int(os.environ.get('HERMES_PTY_PORT', '3001')) +HOST = os.environ.get('HERMES_PTY_HOST', '0.0.0.0') +AUTH_TOKEN = os.environ.get('HERMES_PTY_TOKEN', '') +HERMES_VENV = os.environ.get('HERMES_VENV', '/opt/hermes/venv') +HERMES_DATA = os.environ.get('HERMES_DATA', '/opt/hermes/data') +MAX_SESSIONS = int(os.environ.get('HERMES_MAX_SESSIONS', '5')) +UI_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ui') + +# Load token from UCI if not set via env +if not AUTH_TOKEN: + try: + result = subprocess.run( + ['uci', '-q', 'get', 'hermes.main.pty_token'], + capture_output=True, text=True, timeout=3 + ) + AUTH_TOKEN = result.stdout.strip() + except Exception: + pass + +# ── WebSocket helpers (RFC 6455) ── + +def decode_ws_frame(data): + """Decode a WebSocket frame. Returns (opcode, payload, total_consumed) or None.""" + if len(data) < 2: + return None + opcode = data[0] & 0x0f + masked = bool(data[1] & 0x80) + payload_len = data[1] & 0x7f + offset = 2 + + if payload_len == 126: + if len(data) < 4: + return None + payload_len = struct.unpack('>H', data[2:4])[0] + offset = 4 + elif payload_len == 127: + if len(data) < 10: + return None + payload_len = struct.unpack('>Q', data[2:10])[0] + offset = 10 + + mask = None + if masked: + if len(data) < offset + 4: + return None + mask = data[offset:offset + 4] + offset += 4 + + if len(data) < offset + payload_len: + return None + + payload = bytearray(data[offset:offset + payload_len]) + if mask: + for i in range(len(payload)): + payload[i] ^= mask[i & 3] + + return (opcode, bytes(payload), offset + payload_len) + + +def encode_ws_frame(data, opcode=0x01): + """Encode data into a WebSocket frame.""" + if isinstance(data, str): + payload = data.encode('utf-8') + else: + payload = data + length = len(payload) + + if length < 126: + header = struct.pack('>BB', 0x80 | opcode, length) + elif length < 65536: + header = struct.pack('>BBH', 0x80 | opcode, 126, length) + else: + header = struct.pack('>BBQ', 0x80 | opcode, 127, length) + + return header + payload + + +# ── PTY Session ── + +active_sessions = 0 + + +class PtySession: + def __init__(self, request_handler): + global active_sessions + self.handler = request_handler + self.proc = None + self.cols = 80 + self.rows = 24 + self.alive = True + self.buffer = b'' + self._spawn_fail_count = 0 + self._MAX_SPAWN_RETRIES = 5 + self._ping_timer = None + self._pong_received = True + + active_sessions += 1 + print(f"[hermes-pty] Session created (active: {active_sessions}/{MAX_SESSIONS})", flush=True) + + self._spawn_pty() + + def _spawn_pty(self): + """Spawn the shell process with a PTY.""" + env = os.environ.copy() + env.update({ + 'TERM': 'xterm-256color', + 'COLUMNS': str(self.cols), + 'LINES': str(self.rows), + 'COLORTERM': 'truecolor', + 'LANG': 'en_US.UTF-8', + 'HERMES_VENV': HERMES_VENV, + 'HERMES_DATA': HERMES_DATA, + 'HOME': '/root', + 'PATH': f"{HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + }) + + try: + # Create PTY + master_fd, slave_fd = pty.openpty() + + # Set terminal size + winsize = struct.pack('HHHH', self.rows, self.cols, 0, 0) + fcntl.ioctl(slave_fd, termios.TIOCSWINSZ, winsize) + + # Spawn shell + self.proc = subprocess.Popen( + ['/bin/sh', '-l'], + stdin=slave_fd, + stdout=slave_fd, + stderr=slave_fd, + env=env, + preexec_fn=os.setsid, + ) + os.close(slave_fd) + self.master_fd = master_fd + self._spawn_fail_count = 0 + + # Set master_fd to non-blocking + flags = fcntl.fcntl(master_fd, fcntl.F_GETFL) + fcntl.fcntl(master_fd, fcntl.F_SETFL, flags | os.O_NONBLOCK) + + # Start reader thread + self._reader_thread = threading.Thread(target=self._read_pty, daemon=True) + self._reader_thread.start() + + print(f"[hermes-pty] Shell spawned (PID: {self.proc.pid})", flush=True) + + except Exception as e: + print(f"[hermes-pty] Failed to spawn shell: {e}", flush=True) + self._cleanup() + + def _read_pty(self): + """Read from PTY and send to WebSocket.""" + while self.alive and self.proc and self.proc.poll() is None: + try: + r, _, _ = select.select([self.master_fd], [], [], 0.1) + if r: + data = os.read(self.master_fd, 4096) + if data: + self._send_to_client(data) + except (OSError, IOError): + break + except Exception as e: + if self.alive: + print(f"[hermes-pty] Read error: {e}", flush=True) + break + + if self.alive: + print(f"[hermes-pty] Shell exited", flush=True) + self._spawn_fail_count += 1 + if self._spawn_fail_count > self._MAX_SPAWN_RETRIES: + print(f"[hermes-pty] Shell failed {self._spawn_fail_count} times, stopping", flush=True) + self._send_to_client(b'\r\n\x1b[31mShell failed to start after multiple attempts.\x1b[0m\r\n') + return + + # Auto-restart + self._send_to_client(b'\r\n\x1b[33mShell exited, restarting...\x1b[0m\r\n') + time.sleep(1) + if self.alive: + self._spawn_pty() + + def _send_to_client(self, data): + """Send data to WebSocket client.""" + if not self.alive: + return + try: + frame = encode_ws_frame(data, 0x01) + self.handler.request.sendall(frame) + except Exception: + pass + + def handle_message(self, text): + """Handle incoming WebSocket text message.""" + try: + msg = json.loads(text) + msg_type = msg.get('type', '') + + if msg_type == 'stdin': + data = msg.get('data', '') + # Clean bracketed paste escape sequences + data = data.replace('\x1b[?2004l', '').replace('\x1b[?2004h', '') + data = data.replace('\x1b[200~', '').replace('\x1b[201~', '') + if self.proc and self.proc.poll() is None: + os.write(self.master_fd, data.encode('utf-8')) + + elif msg_type == 'resize': + self.cols = msg.get('cols', 80) + self.rows = msg.get('rows', 24) + if self.master_fd: + winsize = struct.pack('HHHH', self.rows, self.cols, 0, 0) + try: + fcntl.ioctl(self.master_fd, termios.TIOCSWINSZ, winsize) + except Exception: + pass + + elif msg_type == 'ping': + self._send_to_client(json.dumps({'type': 'pong'})) + + except json.JSONDecodeError: + # Plain text, send directly to PTY + if self.proc and self.proc.poll() is None: + os.write(self.master_fd, text.encode('utf-8')) + + def handle_binary(self, data): + """Handle incoming binary WebSocket data.""" + if self.proc and self.proc.poll() is None: + os.write(self.master_fd, data) + + def _cleanup(self): + """Clean up session resources.""" + if not self.alive: + return + self.alive = False + + global active_sessions + active_sessions = max(0, active_sessions - 1) + print(f"[hermes-pty] Session ended (active: {active_sessions}/{MAX_SESSIONS})", flush=True) + + if self.proc and self.proc.poll() is None: + try: + os.kill(-self.proc.pid, signal.SIGTERM) + except Exception: + pass + try: + self.proc.terminate() + except Exception: + pass + + try: + if hasattr(self, 'master_fd'): + os.close(self.master_fd) + except Exception: + pass + + def __del__(self): + self._cleanup() + + +# ── WebSocket Upgrade Handler ── + +class WebSocketHandler(http.server.BaseHTTPRequestHandler): + """HTTP + WebSocket upgrade handler.""" + + sessions = [] + + def log_message(self, format, *args): + """Override to add prefix.""" + print(f"[hermes-pty] {args[0] if args else format}", flush=True) + + def do_OPTIONS(self): + self.send_response(204) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') + self.send_header('Access-Control-Allow-Headers', '*') + self.end_headers() + + def do_GET(self): + path = urllib.parse.urlparse(self.path).path + + if path == '/health': + self.send_response(200) + self.send_header('Content-Type', 'application/json') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(json.dumps({ + 'status': 'ok', + 'port': PORT, + 'uptime': time.time() + }).encode()) + return + + # Serve static files from UI_DIR + if path == '/' or path == '': + path = '/index.html' + + # Security: prevent directory traversal + full_path = os.path.normpath(os.path.join(UI_DIR, path.lstrip('/'))) + if not full_path.startswith(UI_DIR): + self.send_response(403) + self.end_headers() + return + + try: + with open(full_path, 'rb') as f: + data = f.read() + + # Determine content type + ext = os.path.splitext(full_path)[1] + content_types = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css', + '.js': 'application/javascript', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.json': 'application/json', + } + ct = content_types.get(ext, 'application/octet-stream') + + self.send_response(200) + self.send_header('Content-Type', ct) + self.send_header('Cache-Control', 'no-cache' if ext == '.html' else 'max-age=3600') + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('X-Frame-Options', 'ALLOWALL') + self.send_header('Content-Security-Policy', + "default-src * 'unsafe-inline' 'unsafe-eval' data: blob: ws: wss:; frame-ancestors *") + self.end_headers() + self.wfile.write(data) + + except FileNotFoundError: + # Fallback to index.html for SPA routing + try: + with open(os.path.join(UI_DIR, 'index.html'), 'rb') as f: + data = f.read() + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(data) + except FileNotFoundError: + self.send_response(404) + self.end_headers() + + def do_GET_ws(self): + """Handle WebSocket upgrade for GET requests to /ws.""" + self._do_websocket_upgrade() + + def _do_websocket_upgrade(self): + """Perform WebSocket handshake and start PTY session.""" + # Authenticate + parsed = urllib.parse.urlparse(self.path) + params = urllib.parse.parse_qs(parsed.query) + client_token = params.get('token', [''])[0] + + if AUTH_TOKEN and client_token != AUTH_TOKEN: + print(f"[hermes-pty] Auth failed from {self.client_address[0]}", flush=True) + self.send_error(403, 'Forbidden') + return + + # Check session limit + if active_sessions >= MAX_SESSIONS: + print(f"[hermes-pty] Max sessions reached ({active_sessions}/{MAX_SESSIONS})", flush=True) + self.send_error(503, 'Service Unavailable') + return + + # WebSocket handshake + key = self.headers.get('Sec-WebSocket-Key', '') + if not key: + self.send_error(400, 'Missing Sec-WebSocket-Key') + return + + accept = base64.b64encode( + hashlib.sha1((key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').encode()).digest() + ).decode() + + response = ( + 'HTTP/1.1 101 Switching Protocols\r\n' + 'Upgrade: websocket\r\n' + 'Connection: Upgrade\r\n' + f'Sec-WebSocket-Accept: {accept}\r\n' + '\r\n' + ) + self.wfile.write(response.encode()) + self.wfile.flush() + + # Create PTY session + session = PtySession(self) + self.__class__.sessions.append(session) + + # Read WebSocket frames + buffer = b'' + while session.alive: + try: + self.request.setblocking(True) + self.request.settimeout(1.0) + data = self.request.recv(4096) + if not data: + break + buffer += data + + while len(buffer) > 0: + result = decode_ws_frame(buffer) + if result is None: + break + opcode, payload, consumed = result + buffer = buffer[consumed:] + + if opcode == 0x01: # Text + session.handle_message(payload.decode('utf-8', errors='replace')) + elif opcode == 0x02: # Binary + session.handle_binary(payload) + elif opcode == 0x08: # Close + print("[hermes-pty] WS close frame received", flush=True) + session._cleanup() + return + elif opcode == 0x09: # Ping + self.request.sendall(encode_ws_frame(payload, 0x0a)) + elif opcode == 0x0a: # Pong + session._pong_received = True + + except socket.timeout: + continue + except (ConnectionResetError, BrokenPipeError, OSError): + break + except Exception as e: + if session.alive: + print(f"[hermes-pty] Read error: {e}", flush=True) + break + + session._cleanup() + + def do_GET(self): + parsed = urllib.parse.urlparse(self.path) + path = parsed.path + + if path == '/ws' or path.startswith('/ws?'): + self._do_websocket_upgrade() + else: + # Serve static files + if path == '/' or path == '': + path = '/index.html' + + full_path = os.path.normpath(os.path.join(UI_DIR, path.lstrip('/'))) + if not full_path.startswith(UI_DIR): + self.send_response(403) + self.end_headers() + return + + try: + with open(full_path, 'rb') as f: + data = f.read() + + ext = os.path.splitext(full_path)[1] + content_types = { + '.html': 'text/html; charset=utf-8', + '.css': 'text/css', + '.js': 'application/javascript', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.json': 'application/json', + } + ct = content_types.get(ext, 'application/octet-stream') + + self.send_response(200) + self.send_header('Content-Type', ct) + self.send_header('Cache-Control', 'no-cache' if ext == '.html' else 'max-age=3600') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(data) + + except FileNotFoundError: + try: + with open(os.path.join(UI_DIR, 'index.html'), 'rb') as f: + data = f.read() + self.send_response(200) + self.send_header('Content-Type', 'text/html; charset=utf-8') + self.send_header('Access-Control-Allow-Origin', '*') + self.end_headers() + self.wfile.write(data) + except FileNotFoundError: + self.send_response(404) + self.end_headers() + + +# ── Main ── + +if __name__ == '__main__': + import socket + + socketserver.TCPServer.allow_reuse_address = True + + class ReusableTCPServer(socketserver.TCPServer): + allow_reuse_address = True + + def handle_request_noblock(self): + try: + request, client_address = self.get_request() + except OSError: + return + if self.verify_request(request, client_address): + try: + self.process_request(request, client_address) + except Exception: + self.handle_error(request, client_address) + self.shutdown_request(request) + else: + self.shutdown_request(request) + + server = ReusableTCPServer((HOST, PORT), WebSocketHandler) + print(f"[hermes-pty] HTTP listening on {HOST}:{PORT}", flush=True) + print(f"[hermes-pty] UI directory: {UI_DIR}", flush=True) + + def shutdown(signum, frame): + print("[hermes-pty] Shutdown", flush=True) + server.shutdown() + sys.exit(0) + + signal.signal(signal.SIGTERM, shutdown) + signal.signal(signal.SIGINT, shutdown) + + try: + server.serve_forever() + except KeyboardInterrupt: + shutdown(None, None) diff --git a/luci-app-hermes/root/usr/share/luci/menu.d/luci-app-hermes.json b/luci-app-hermes/root/usr/share/luci/menu.d/luci-app-hermes.json new file mode 100644 index 000000000..a46042e18 --- /dev/null +++ b/luci-app-hermes/root/usr/share/luci/menu.d/luci-app-hermes.json @@ -0,0 +1,14 @@ +{ + "admin/services/hermes": { + "title": "Hermes Agent", + "order": 90, + "action": { + "type": "view", + "path": "hermes" + }, + "depends": { + "acl": ["luci-app-hermes"], + "uci": { "hermes": true } + } + } +} diff --git a/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json b/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json index a07cd290d..4533012cb 100644 --- a/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json +++ b/luci-app-hermes/root/usr/share/rpcd/acl.d/luci-app-hermes.json @@ -1,11 +1,29 @@ { "luci-app-hermes": { - "description": "Grant UCI access for luci-app-hermes", + "description": "Grant access for luci-app-hermes", "read": { - "uci": [ "hermes" ] + "uci": ["hermes"], + "ubus": { + "service": ["list"], + "file": ["exec", "read", "stat"] + }, + "file": { + "/usr/share/hermes/luci-helper": ["exec"], + "/etc/init.d/hermes": ["exec"], + "/usr/share/hermes/VERSION": ["read"], + "/tmp/hermes-setup.log": ["read"], + "/tmp/hermes-setup.exit": ["read"] + } }, "write": { - "uci": [ "hermes" ] + "uci": ["hermes"], + "ubus": { + "file": ["exec", "write"] + }, + "file": { + "/usr/share/hermes/luci-helper": ["exec"], + "/etc/init.d/hermes": ["exec"] + } } } } diff --git a/luci-app-hermes/scripts/install.sh b/luci-app-hermes/scripts/install.sh new file mode 100755 index 000000000..91efaf6a8 --- /dev/null +++ b/luci-app-hermes/scripts/install.sh @@ -0,0 +1,78 @@ +#!/bin/sh +# ============================================================================= +# Hermes Agent LuCI 插件一键安装脚本 +# 用法: sh -c "$(wget -qO- https://cdn.jsdelivr.net/gh/Boos4721/luci-app-hermes@main/scripts/install.sh)" +# ============================================================================= +set -e + +REPO="Boos4721/luci-app-hermes" +GITHUB_RELEASE="https://github.com/${REPO}/releases/latest/download" + +_ok() { echo " [✓] $*"; } +_warn() { echo " [!] $*"; } +_err() { echo " [✗] $*"; echo ""; exit 1; } + +_download() { + local url="$1" out="$2" + if command -v wget >/dev/null 2>&1; then + wget -q --no-check-certificate -O "$out" "$url" 2>/dev/null + elif command -v curl >/dev/null 2>&1; then + curl -fsSL --connect-timeout 15 -o "$out" "$url" 2>/dev/null + else + _err "未找到 wget 或 curl" + fi +} + +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ Hermes Agent LuCI 插件一键安装 ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +# ── 检查环境 ── +[ -f /etc/openwrt_release ] || _err "仅支持 OpenWrt / ImmortalWrt 系统" +command -v opkg >/dev/null 2>&1 || _err "未找到 opkg,仅支持 OpenWrt 系统" + +ARCH=$(uname -m) +DISTRIB=$(grep -o 'DISTRIB_DESCRIPTION="[^"]*"' /etc/openwrt_release 2>/dev/null | cut -d'"' -f2 || echo "OpenWrt") +_ok "系统: ${DISTRIB}" +_ok "架构: ${ARCH}" +echo "" + +# ── 下载 ipk ── +IPK="/tmp/luci-app-hermes_$$.ipk" +_warn "正在下载 luci-app-hermes..." + +# 主源:GitHub Releases +if ! _download "${GITHUB_RELEASE}/luci-app-hermes_all.ipk" "${IPK}"; then + _warn "GitHub 下载失败,尝试备用源..." + _download "https://github.com/${REPO}/releases/latest/download/luci-app-hermes_all.ipk" "${IPK}" || \ + _err "下载失败,请检查网络连接后重试" +fi + +SIZE=$(wc -c < "${IPK}" 2>/dev/null || echo 0) +[ "${SIZE}" -gt 5000 ] || _err "下载文件无效 (${SIZE} 字节),请稍后重试" +_ok "下载完成 ($(du -sh "${IPK}" 2>/dev/null | cut -f1))" + +# ── 安装 ── +_warn "正在安装..." +opkg update >/dev/null 2>&1 || true + +if ! opkg install --force-reinstall "${IPK}" 2>&1; then + _warn "首次安装失败,尝试强制安装..." + opkg install --force-reinstall --force-depends "${IPK}" || \ + _err "安装失败,请查看上方错误信息" +fi + +rm -f "${IPK}" +rm -f /tmp/luci-indexcache 2>/dev/null || true +rm -rf /tmp/luci-modulecache 2>/dev/null || true + +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ [OK] 安装完成! ║" +echo "║ ║" +echo "║ → LuCI → 服务 → Hermes Agent ║" +echo "║ → 点击「更多」→「安装环境」完成 Python + Hermes 安装 ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" diff --git a/luci-app-podman/.editorconfig b/luci-app-podman/.editorconfig new file mode 100644 index 000000000..6a9ee691d --- /dev/null +++ b/luci-app-podman/.editorconfig @@ -0,0 +1,43 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +# JavaScript +[*.js] +indent_style = tab +indent_size = 4 +max_line_length = 110 + +# YAML +[*.{yml,yaml}] +indent_style = space +indent_size = 2 +max_line_length = 110 + +# Markdown +[*.md] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = false +max_line_length = 110 + +# POT translation templates +[*.pot] +indent_style = space +indent_size = 2 + +# Makefile +[Makefile] +indent_style = tab +indent_size = 4 + +# Shell scripts (.sh and .podman) +[*.{sh,podman}] +indent_style = tab +indent_size = 4 +end_of_line = lf +max_line_length = 110 diff --git a/luci-app-podman/.gitignore b/luci-app-podman/.gitignore new file mode 100644 index 000000000..554de31ee --- /dev/null +++ b/luci-app-podman/.gitignore @@ -0,0 +1,7 @@ +docs/swagger-latest.yaml +.claude +CLAUDE.md +.DS_Store +*.bak +*-cleaned.pot +.mcp.json diff --git a/luci-app-podman/.jsbeautifyrc b/luci-app-podman/.jsbeautifyrc new file mode 100644 index 000000000..58217af01 --- /dev/null +++ b/luci-app-podman/.jsbeautifyrc @@ -0,0 +1,7 @@ +{ + "indent_with_tabs": true, + "editorconfig": true, + "space_after_anon_function": true, + "jslint_happy": true, + "wrap_line_length": 110 +} diff --git a/luci-app-podman/.scripts/check-translations.sh b/luci-app-podman/.scripts/check-translations.sh new file mode 100755 index 000000000..8e51b5d11 --- /dev/null +++ b/luci-app-podman/.scripts/check-translations.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +RED='\033[0;31m' +GREEN='\033[0;32m' +BLUE='\033[0;34m' +BOLD='\033[1m' +NC='\033[0m' + +PO_FILE="$1" +JS_DIR="$2" + +if [[ -z "$PO_FILE" || -z "$JS_DIR" ]]; then + echo -e "${BOLD}Usage:${NC} $0 " + exit 1 +fi + +TEMP_DEFINED=$(mktemp) +TEMP_USED=$(mktemp) + +echo -e "${BLUE}Read PO-File: $PO_FILE${NC}" +grep '^\s*msgid "' "$PO_FILE" | sed 's/^\s*msgid "//;s/"$//' | grep -v '^$' | sort -u > "$TEMP_DEFINED" + +COUNT_DEFINED=$(wc -l < "$TEMP_DEFINED" | xargs) +echo " -> $COUNT_DEFINED IDs found." + +echo -e "${BLUE}Search JS-Files recursive in: $JS_DIR${NC}" + +grep -r -h -o -E "_\(['\"][^'\"]+['\"]\)" "$JS_DIR" --include="*.js" > "$TEMP_USED" + +sed -i '' -E "s/^_\(['\"]//;s/['\"]\)$//" "$TEMP_USED" + +sort -u "$TEMP_USED" -o "$TEMP_USED" + +COUNT_USED=$(wc -l < "$TEMP_USED" | xargs) +echo " -> $COUNT_USED used Strings found." + +echo "---------------------------------------------------" + +echo -e "${BOLD}Not used Message IDs (in PO, not in Code):${NC}" +comm -23 "$TEMP_DEFINED" "$TEMP_USED" > "unused_ids.txt" +if [[ -s "unused_ids.txt" ]]; then + cat "unused_ids.txt" +else + echo -e "${GREEN}None. all clean!${NC}" +fi + +echo "---------------------------------------------------" + +echo -e "${BOLD}Not defined Message IDs (in Code, missing in PO):${NC}" +comm -13 "$TEMP_DEFINED" "$TEMP_USED" > "missing_ids.txt" +if [[ -s "missing_ids.txt" ]]; then + cat "missing_ids.txt" +else + echo -e "${GREEN}None. All here!${NC}" +fi + +# Aufräumen +rm "$TEMP_DEFINED" "$TEMP_USED" "unused_ids.txt" "missing_ids.txt" 2>/dev/null diff --git a/luci-app-podman/.scripts/update-pot.sh b/luci-app-podman/.scripts/update-pot.sh new file mode 100755 index 000000000..d0f1f8832 --- /dev/null +++ b/luci-app-podman/.scripts/update-pot.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# +# POT File Updater for LuCI App Podman +# Regenerates the POT file from JavaScript source code +# Supports both singular _('...') and plural N_(n, '...', '...') forms +# + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +POT_FILE="$PROJECT_ROOT/po/templates/podman.pot" +HTDOCS_DIR="$PROJECT_ROOT/htdocs" + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' # No Color + +# Temp files +PLURAL_PAIRS=$(mktemp) +PLURAL_SKIP=$(mktemp) +CODE_STRINGS=$(mktemp) +FILTERED_STRINGS=$(mktemp) + +# Cleanup on exit +trap "rm -f $PLURAL_PAIRS $PLURAL_SKIP $CODE_STRINGS $FILTERED_STRINGS" EXIT + +echo "POT File Updater for LuCI App Podman" +echo "=====================================" +echo "" + +# Step 1: Extract plural forms N_(count, 'singular', 'plural') +echo "Extracting plural forms..." + +find "$HTDOCS_DIR" -name "*.js" -exec cat {} \; | \ + perl -0777 -ne "while (/N_\s*\([^,]+,\s*'([^']+)'\s*,\s*'([^']+)'\s*\)/gs) { print \"\$1|\$2\n\"; }" | \ + sort -u > "$PLURAL_PAIRS" + +PLURAL_COUNT=$(wc -l < "$PLURAL_PAIRS" | tr -d ' ') +# Handle empty file (wc returns 1 for empty file with no newline) +if [ ! -s "$PLURAL_PAIRS" ]; then + PLURAL_COUNT=0 +fi +echo "Found $PLURAL_COUNT plural form(s)" + +# Step 2: Build skip list (both singular and plural strings from N_() calls) +# These should not appear as standalone msgid entries +cat "$PLURAL_PAIRS" | while IFS='|' read -r singular plural; do + echo "$singular" + echo "$plural" +done | sort -u > "$PLURAL_SKIP" + +# Step 3: Extract all singular strings _('...') +echo "Extracting singular strings..." + +find "$HTDOCS_DIR" -name "*.js" -exec cat {} \; | \ + perl -0777 -ne "while (/(? "$CODE_STRINGS" + +TOTAL_SINGULAR=$(wc -l < "$CODE_STRINGS" | tr -d ' ') + +# Step 4: Filter out strings that are covered by plural forms +if [ -s "$PLURAL_SKIP" ]; then + grep -vxFf "$PLURAL_SKIP" "$CODE_STRINGS" > "$FILTERED_STRINGS" || true +else + cp "$CODE_STRINGS" "$FILTERED_STRINGS" +fi + +FILTERED_COUNT=$(wc -l < "$FILTERED_STRINGS" | tr -d ' ') +SKIPPED=$((TOTAL_SINGULAR - FILTERED_COUNT)) + +if [ $SKIPPED -gt 0 ]; then + echo -e "${YELLOW}Skipped $SKIPPED string(s) covered by plural forms${NC}" +fi +echo "Found $FILTERED_COUNT unique singular string(s)" + +# Step 5: Generate POT file +echo "" +echo "Generating POT file..." + +# Write header +cat > "$POT_FILE" << 'EOF' +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +EOF + +# Write plural entries +if [ -s "$PLURAL_PAIRS" ]; then + while IFS='|' read -r singular plural; do + echo "msgid \"$singular\"" + echo "msgid_plural \"$plural\"" + echo "msgstr[0] \"\"" + echo "msgstr[1] \"\"" + echo "" + done < "$PLURAL_PAIRS" >> "$POT_FILE" +fi + +# Write singular entries +while IFS= read -r line; do + # Skip empty lines + [ -z "$line" ] && continue + echo "msgid \"$line\"" + echo "msgstr \"\"" + echo "" +done < "$FILTERED_STRINGS" >> "$POT_FILE" + +TOTAL_ENTRIES=$((PLURAL_COUNT + FILTERED_COUNT)) + +echo "" +echo -e "${GREEN}POT file updated successfully!${NC}" +echo "Location: $POT_FILE" +echo "Entries: $TOTAL_ENTRIES ($PLURAL_COUNT plural, $FILTERED_COUNT singular)" +echo "" +echo "Note: Remember to update translation files (po/*/podman.po) with new strings." diff --git a/luci-app-podman/LICENSE b/luci-app-podman/LICENSE new file mode 100644 index 000000000..7be6caebd --- /dev/null +++ b/luci-app-podman/LICENSE @@ -0,0 +1,176 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/luci-app-podman/Makefile b/luci-app-podman/Makefile new file mode 100644 index 000000000..edd5b7e8a --- /dev/null +++ b/luci-app-podman/Makefile @@ -0,0 +1,20 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME := luci-app-podman +PKG_VERSION := 2.3.4 +PKG_RELEASE := 1 +PKG_URL := https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman +PKG_MAINTAINER := Christopher Söllinger +PKG_LICENSE := Apache-2.0 +PKG_LICENSE_FILES := LICENSE + +PKG_BUILD_DEPENDS := podman + +LUCI_TITLE := LuCI Podman Application +LUCI_DESCRIPTION := LuCI Support for podman +LUCI_DEPENDS := +luci-base +rpcd +rpcd-mod-ucode +ucode-mod-socket +ucode-mod-struct +ucode-mod-uloop +ucode-mod-fs +ucode-mod-html +ucode-mod-uci +liblucihttp-ucode +coreutils-timeout +LUCI_PKGARCH := all + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-podman/README.md b/luci-app-podman/README.md new file mode 100644 index 000000000..632fafe1a --- /dev/null +++ b/luci-app-podman/README.md @@ -0,0 +1,161 @@ +[![OpenWrt](https://img.shields.io/badge/OpenWrt-25.12-darkgreen.svg?logo=openwrt&color=002B49)](https://openwrt.org/) +[![OpenWrt](https://img.shields.io/badge/OpenWrt-24.10-darkgreen.svg?logo=openwrt&color=002B49)](https://openwrt.org/) +[![GitHub Release](https://img.shields.io/github/v/release/Zerogiven-OpenWRT-Packages/luci-app-podman?logo=github)](https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman/releases) +[![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/Zerogiven-OpenWRT-Packages/luci-app-podman/total?color=blue&logo=github)](https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman/releases) +[![GitHub Issues or Pull Requests](https://img.shields.io/github/issues/Zerogiven-OpenWRT-Packages/luci-app-podman?logo=github)](https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman/issues) + +# LuCI App Podman + +Modern and feature rich LuCI web interface for managing Podman on OpenWrt. Some ideas which may come in one of the next version(s) can you find in the [todo's](./docs/TODO.md) file. Also feel free to open a ticket or send me a mail if you have suggestions, ideas or problems 🙂 + +
+ +Navigation + +- [Features](#features) +- [Screenshots](#screenshots) +- [Requirements](#requirements) +- [Installation](#installation) +- [Usage](#usage) +- [Container Auto-Update](#container-auto-update) +- [Container Auto-Start](#container-auto-start) +- [Configuration](#configuration) +- [Credits](#credits) + +
+ +## Features + +- **Container Management**: Start, stop, restart, create, remove containers +- **Live Streaming**: Real-time logs, stats, and process list streamed directly via a dedicated ucode controller +- **Container Auto-Update**: Check for image updates and recreate containers with latest images (see [Auto-Update](#container-auto-update)) +- **Auto-start Support**: Automatic init script generation for containers with restart policies +- **Image Management**: Pull, remove, inspect images with streaming progress +- **Volume Management**: Create, delete, export/import volumes with tar backups +- **Network Management**: Bridge, macvlan, ipvlan with VLAN support and optional OpenWrt integration (auto-creates bridge devices, network interfaces, dnsmasq exclusion, and shared `podman` firewall zone with DNS access rules) +- **Pod Management**: Create pods with shared namespaces, lifecycle actions (start/stop/restart/pause/unpause), and add containers to existing pods directly from the create form +- **Secret Management**: Encrypted storage for sensitive data +- **System Overview**: Resource usage, system-wide cleanup +- **Multilingual**: de, es, fr, nl, ru, zh_Hans, zh_Hant + +## Screenshots + +| Overview | Container List | Container Info | +|---|---|---| +| ![Overview](docs/screenshots/overview.png) | ![Container List](docs/screenshots/container-list.png) | ![Container Info](docs/screenshots/container-info.png) | + +| Live Logs | Image List | Network Create | +|---|---|---| +| ![Logs](docs/screenshots/container-log.png) | ![Images](docs/screenshots/image-list.png) | ![Network OpenWrt](docs/screenshots/network-create.png) | + +## Requirements + +- OpenWrt 25.12 / 24.10 +- Dependencies: + - `podman` + - `luci-base` + - `rpcd` + - `rpcd-mod-ucode` + - `ucode-mod-socket / -struct / -uloop / -fs / -html / -uci` + - `liblucihttp-ucode` + - `coreutils-timeout` +- Sufficient storage for images/containers + +## Installation + +### From Package Feed + +You can setup this package feed to install and update it with apk/opkg: + +[https://github.com/Zerogiven-OpenWRT-Packages/package-feed](https://github.com/Zerogiven-OpenWRT-Packages/package-feed) + +### From Package + +**OpenWrt 25.12** (apk): +```bash +wget https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman/releases/download/v2.3.4/luci-app-podman_2.3.4-r1_all.apk +apk update && apk add --allow-untrusted luci-app-podman_2.3.4-r1_all.apk +``` + +**OpenWrt 24.10** (opkg): +```bash +wget https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman/releases/download/v2.3.4/luci-app-podman_2.3.4-r1_all.ipk +opkg update && opkg install luci-app-podman_2.3.4-r1_all.ipk +``` + +### From Source + +```bash +git clone https://github.com/Zerogiven-OpenWRT-Packages/luci-app-podman.git package/luci-app-podman +make menuconfig # Navigate to: LuCI → Applications → luci-app-podman,Utilities → podman +make package/luci-app-podman/compile V=s +``` + +## Usage + +Access via **Podman** in LuCI, or directly at: + +``` +http://your-router-ip/cgi-bin/luci/admin/podman +``` + +If encountering socket errors: + +```bash +/etc/init.d/podman start +/etc/init.d/podman enable +``` + +## Container Auto-Start + +You can generate a startup script which respects the restart policy set in the container. + +> [!TIP] +> If you want save startup scripts during an upgrade you have to config your `/etc/sysupgrade.conf` with `/etc/init.d/podman-container-*`. +> ```bash +> echo "/etc/init.d/podman-container-*" >> /etc/sysupgrade.conf +> ``` + +## Container Auto-Update + +The auto-update feature checks for newer container images and recreates containers with the updated images while preserving all configuration. + +### Setup + +To enable auto-update for a container, add the label when creating it: + +```bash +podman run -d --name mycontainer \ + --label io.containers.autoupdate=registry \ + nginx:latest +``` + +Or add via the LuCI interface in the container creation form under "Labels". + +### How to Update + +1. Go to **Podman → Overview** +2. Click **"Check for Updates"** in the System Maintenance section +3. The system compares image digests without pulling (no bandwidth used until update) +4. Select which containers to update +5. Click **"Update Selected"** to pull new images and recreate containers + +Container names and init scripts are preserved - no manual reconfiguration needed. + +## Configuration + +The app stores its settings in `/etc/config/luci-podman`: + +| Option | Default | Description | +|--------|---------|-------------| +| `init_start_priority` | `99` | procd start priority for container init scripts (max 99: procd sorts rc.d symlinks as strings, so 100+ breaks ordering) | +| `socket_path` | `/run/podman/podman.sock` | Path to the Podman API socket | + +## Credits + +Inspired by: + +- [openwrt-podman](https://github.com/breeze303/openwrt-podman/) - Podman on OpenWrt +- [openwrt/luci-app-dockerman](https://github.com/openwrt/luci/tree/master/applications/luci-app-dockerman) - Docker LuCI design patterns +- [lisaac/luci-app-dockerman](https://github.com/lisaac/luci-app-dockerman) - Docker LuCI design patterns +- [OpenWrt Podman Guide](https://openwrt.org/docs/guide-user/virtualization/podman) - Official documentation diff --git a/luci-app-podman/docs/TODO.md b/luci-app-podman/docs/TODO.md new file mode 100644 index 000000000..336eeb504 --- /dev/null +++ b/luci-app-podman/docs/TODO.md @@ -0,0 +1,80 @@ +# ToDo's / Known issues + +## Common + +- Optimize Mobile View (4) +- Checkout new table filters for 25.12 and backwards compatibility (4) +- Sortable table (3) +- Check for duplicated code (2) + +## Overview + +- Configuration Tab (2) + +### Prune + +- Long running prune fails after 50s (1) + +### Auto Update + +- If re-create container fails, it would be nice if user could re-create the container. Some Ideas: (2) + 1. Show cli command + 2. Show spec data and make it possible to import + +## Container + +- Checkpoints export/import (3) + +### Add + +- Missing restartRetries (3) +- Missing health check settings (2) +- Fill form from cli command (3) +- Timezone field (3) + +### List + +### Details + +- Re-create container with new image (4) +- Add missing restartRetries (3) +- Healthcheck tab with form and manual health check action (2) +- Init.d priority editable. Maybe directly in the file. (3) +- Add some more informations (3) + - devices +- Attach to a container (ttyd) (3) +- Add devices show/add/remove (3) +- Add advanced update tab. Pure JSON which gets sent to update endpoint (2) +- Timezone field (3) + +## Pods + +- Think how to handle restart policy (2) + +### List + +- Replace table from container modal with one from jsapi (2) + +### Details + +- In containers tab add create/remove/refresh button (2) + +## Volumes + +### List + +- Big exports which takes more than 50s are failing (2) + +## Networks + +### Details + +- Create detail page (2) +- Update network (2) +- Disconnect container (2) + +## Secrets + +### Add + +### List diff --git a/luci-app-podman/docs/explicitly-excluded-features.md b/luci-app-podman/docs/explicitly-excluded-features.md new file mode 100644 index 000000000..5921b41a2 --- /dev/null +++ b/luci-app-podman/docs/explicitly-excluded-features.md @@ -0,0 +1,27 @@ +# 🚫 Explicitly Excluded Features + +The following features were considered but explicitly excluded: + +## Prune Operations +- ❌ Container Prune - Already available via System Prune in Overview +- ❌ Image Prune - Already available via System Prune in Overview +- ❌ Network Prune - Already available via System Prune in Overview +- ❌ Volume Prune - Already available via System Prune in Overview +- ❌ Pod Prune - Already available via System Prune in Overview + +**Reason:** Global system prune in overview.js handles all cleanup operations + +## Image Management +- ❌ Image Tag - Not needed for typical use cases +- ❌ Image History - Not needed for typical use cases + +**Reason:** Low value for typical OpenWrt/embedded use cases + +## Advanced Features +- ❌ Container Exec - Too complex, SSH/CLI available +- ❌ Container Archive (Copy Files) - Too complex UI +- ❌ Container Checkpoint/Restore - May not work on OpenWrt (needs CRIU) +- ❌ Image Build - Too complex, build elsewhere and pull +- ❌ Generate Systemd Units - OpenWrt doesn't use systemd +- ❌ Manifest Management - Too niche +- ❌ System Events (Real-time Updates) - Events disabled on system (`events_logger = "none"`) diff --git a/luci-app-podman/docs/screenshots/container-health.png b/luci-app-podman/docs/screenshots/container-health.png new file mode 100644 index 000000000..1abb80a69 Binary files /dev/null and b/luci-app-podman/docs/screenshots/container-health.png differ diff --git a/luci-app-podman/docs/screenshots/container-info.png b/luci-app-podman/docs/screenshots/container-info.png new file mode 100644 index 000000000..ec0795e4b Binary files /dev/null and b/luci-app-podman/docs/screenshots/container-info.png differ diff --git a/luci-app-podman/docs/screenshots/container-list.png b/luci-app-podman/docs/screenshots/container-list.png new file mode 100644 index 000000000..f81382c69 Binary files /dev/null and b/luci-app-podman/docs/screenshots/container-list.png differ diff --git a/luci-app-podman/docs/screenshots/container-log.png b/luci-app-podman/docs/screenshots/container-log.png new file mode 100644 index 000000000..802a4502e Binary files /dev/null and b/luci-app-podman/docs/screenshots/container-log.png differ diff --git a/luci-app-podman/docs/screenshots/container-resource.png b/luci-app-podman/docs/screenshots/container-resource.png new file mode 100644 index 000000000..6774f0317 Binary files /dev/null and b/luci-app-podman/docs/screenshots/container-resource.png differ diff --git a/luci-app-podman/docs/screenshots/container-stats.png b/luci-app-podman/docs/screenshots/container-stats.png new file mode 100644 index 000000000..a20ad0623 Binary files /dev/null and b/luci-app-podman/docs/screenshots/container-stats.png differ diff --git a/luci-app-podman/docs/screenshots/image-list.png b/luci-app-podman/docs/screenshots/image-list.png new file mode 100644 index 000000000..4bf6f08bc Binary files /dev/null and b/luci-app-podman/docs/screenshots/image-list.png differ diff --git a/luci-app-podman/docs/screenshots/network-create.png b/luci-app-podman/docs/screenshots/network-create.png new file mode 100644 index 000000000..86b94f4a4 Binary files /dev/null and b/luci-app-podman/docs/screenshots/network-create.png differ diff --git a/luci-app-podman/docs/screenshots/network-setup-openwrt-integration.png b/luci-app-podman/docs/screenshots/network-setup-openwrt-integration.png new file mode 100644 index 000000000..e46b8dff6 Binary files /dev/null and b/luci-app-podman/docs/screenshots/network-setup-openwrt-integration.png differ diff --git a/luci-app-podman/docs/screenshots/overview.png b/luci-app-podman/docs/screenshots/overview.png new file mode 100644 index 000000000..32b8d59a0 Binary files /dev/null and b/luci-app-podman/docs/screenshots/overview.png differ diff --git a/luci-app-podman/docs/screenshots/screenshots.gif b/luci-app-podman/docs/screenshots/screenshots.gif new file mode 100644 index 000000000..b6618375e Binary files /dev/null and b/luci-app-podman/docs/screenshots/screenshots.gif differ diff --git a/luci-app-podman/docs/screenshots/volume-inspect.png b/luci-app-podman/docs/screenshots/volume-inspect.png new file mode 100644 index 000000000..9d1bc4650 Binary files /dev/null and b/luci-app-podman/docs/screenshots/volume-inspect.png differ diff --git a/luci-app-podman/docs/testing-manual.md b/luci-app-podman/docs/testing-manual.md new file mode 100644 index 000000000..fde4fd99d --- /dev/null +++ b/luci-app-podman/docs/testing-manual.md @@ -0,0 +1,365 @@ +# Manual UI Testing Plan + +1. xx + +## 0. Authentication + +- [x] **0.1** Navigate to `http:///cgi-bin/luci` → Redirected to login page +- [x] **0.2** Submit correct credentials → Redirected to LuCI dashboard +- [x] **0.3** Navigate to `/admin/podman` → Overview page loads without re-auth +- [x] **0.4** Open browser Console tab → No JS errors + +--- + +## 1. Overview (`/admin/podman`) + +- [x] **1.1** Load overview → Page loads; all counters visible (0 is fine) +- [x] **1.2** Podman version string visible → Not empty, not "undefined" +- [ ] **1.3** System status indicator visible → Shows running (socket available) +- [x] **1.4** Note all current counts → Baseline for comparison after later steps + +### 1a. Auto-update modal - live progress + +Prereq: at least one container with label `io.containers.autoupdate=registry`. + +- [ ] **1.5** Click "Check for Updates" → Spinner per container → "Apply Updates" modal lists candidates +- [ ] **1.6** Tick a container with an actually-newer image upstream → Click Update → Modal switches to log view with `Update image: 1/N - ` header, spinner running, Close button **disabled** +- [ ] **1.7** Log shows `Pulling `, then live `Trying to pull...`, `Copying blob ...` lines - progress lines with CR overwrite the previous line (no flood of "Copying blob X 50%, 51%, 52%, ...") +- [ ] **1.8** After pull, log shows stage markers: `→ Stopping container`, `→ Removing container`, `→ Creating container`, `→ Starting container`, `→ Reinstating init script`, `✓ Updated to ` +- [ ] **1.9** Multi-container update: each container delimited by `━━ ━━`; previous container's log remains visible above current one +- [ ] **1.10** After loop ends, counter shows `Done.`, spinner stops, Close button **enabled** +- [ ] **1.11** Click Close → modal dismisses → summary alert appears (`Containers updated successfully` for full success, list of failures otherwise) +- [ ] **1.12** Pull failure (rename the container's image to a non-existent registry before running, or change tag to bogus): log shows `✗ `, run continues to next container, summary marks failed +- [ ] **1.13** Mid-flight failure (SSH `podman run -d --name alpine sleep 3600` between Stop and Create, or simulate by other means): log shows `✗ Create failed: ...` for that container, summary marks failed +- [ ] **1.14** Old-image cleanup: after successful update of one container, log shows `━━ Cleaning up old images... ━━` then either `✓ Removed old image ` or `! Old image could not be removed - still in use by another container` +- [ ] **1.15** Modal scroll: when log overflows visible area, content auto-scrolls to bottom on append; scroll up manually → new lines append at bottom but viewport stays where the user put it (sticky-scroll) +- [ ] **1.16** Long pull crossing uhttpd `script_timeout` (~55s): server-side connection rotates transparently (auto-reconnect in `Model._stream`); log keeps growing, no spurious error + +--- + +## 2. Images (`/admin/podman/images`) + +- [x] **2.1** Load images list → Page loads (may be empty) +- [x] **2.2** Click "Pull Image" → Form/modal appears with name input +- [x] **2.3** Submit empty form → Validation error, no request sent +- [x] **2.4** Pull nonexistent image (e.g. `this-does-not-exist:fake`) → Error notification shown, list unchanged +- [x] **2.5** Pull `alpine:latest` → Progress indicator → success notification → `alpine` appears in list +- [ ] **2.6** Pull `alpine:latest` again (already exists) → Graceful result (no crash, may show "already up to date") +- [x] **2.7** Click inspect on `alpine` → Modal opens with JSON image data +- [x] **2.8** Return to overview → Image count incremented by 1 + +--- + +## 3. Containers (`/admin/podman/containers`) + +*Requires `alpine:latest` from step 2.5.* + +### 3a. Create + +- [x] **3.1** Load containers list → Page loads (may be empty) +- [x] **3.2** Click "Create Container" → Form modal opens +- [x] **3.3** Submit empty form → Validation errors on required fields +- [x] **3.4** Fill name: `test-alpine`, image: `alpine:latest`, no other changes → Submit → Container created; `test-alpine` visible in list +- [x] **3.5** Return to overview → Container count incremented + +### 3b. List operations + +- [x] **3.6** Verify `test-alpine` row shows correct Name, Image, State +- [x] **3.7** Select `test-alpine` checkbox → Only that row selected +- [x] **3.8** Click top "select all" checkbox → All rows selected +- [x] **3.9** Click top checkbox again → All deselected + +### 3c. Start / Stop / Restart + +- [x] **3.10** If container is stopped: select → Start → State changes to `Running`; success notification +- [x] **3.11** Select running container → Stop → State changes to `Exited`; success notification +- [x] **3.12** Select stopped container → Restart → State changes to `Running` + +### 3d. Detail view - Info tab + +- [x] **3.13** Click `test-alpine` name link → Navigates to container detail page +- [x] **3.14** Name, image, state displayed → Matches list view +- [x] **3.15** Edit container name to `test-alpine-renamed` → Save → Name updated; notification shown +- [x] **3.16** Change restart policy to `always` → Save → Policy updated; success notification +- [x] **3.17** Init script auto-generated → Auto-start indicator shows enabled +- [x] **3.18** Click "Show Init Script" → Modal with script content; script references `test-alpine-renamed` +- [x] **3.19** Toggle init script disabled → Indicator changes +- [x] **3.20** Toggle init script enabled again → Indicator reverts +- [x] **3.21** Change restart policy to `no` → Save → Init script removed/disabled +- [x] **3.22** Navigate back to list → Name shown as `test-alpine-renamed` + +### 3e. Detail view - Resources tab + +- [x] **3.23** Click Resources tab → Tab activates; current limits shown +- [ ] **3.24** Set CPU limit to `0.5` → Save → After page reload, Resources tab is active (tab persistence) +- [x] **3.25** Set Memory limit to `64m` → Save → Value persisted +- [x] **3.26** Enter invalid CPU value (e.g. `abc`) → Save → Validation error; no save + +### 3f. Detail view - Stats tab + +- [x] **3.27** Start container (if stopped) +- [x] **3.28** Click Stats tab → CPU%, Memory, Net I/O data appears +- [x] **3.29** Stop container; click Stats tab → Graceful empty/zero state, no JS crash + +### 3g. Detail view - Logs tab + +- [x] **3.30** Click Logs tab → Log output area visible (may be empty for alpine) +- [x] **3.31** Change line count to `50` → Apply → Fetches last 50 lines +- [ ] **3.32** Enable live streaming toggle → Toggle activates +- [x] **3.33** Disable live streaming → Streaming stops, toggle off +- [x] **3.34** Click Clear → Log area clears + +### 3h. Detail view - Processes tab + +- [x] **3.35** Start container; click Processes tab → Process table appears +- [x] **3.36** Stop container; click Processes tab → Graceful empty/error state, no JS crash + +### 3i. Network management (on detail page) + +- [x] **3.37** Note current networks attached → Baseline +- [x] **3.38** Disconnect default network → Network removed from list +- [x] **3.39** Reconnect the network → Network added back +- [x] **3.40** Connect a network with valid static IP → Network added with IP shown +- [x] **3.41** Connect a network with invalid IP → Validation error + +### 3j. Pause / Unpause + +- [ ] **3.42** List view: select a running container → click ⏸⏸ Pause → state changes to `paused`; success notification +- [ ] **3.43** List view: same paused container selected → click ▶ Start → state changes back to `running` (Start polymorphically unpauses paused containers) +- [ ] **3.44** Detail view: container is running → ▶ Start is highlighted as active, ⏸⏸ Pause is clickable +- [ ] **3.45** Detail view: click Pause → reload → state is `paused`, ⏸⏸ Pause is now highlighted, ▶ Start is clickable +- [ ] **3.46** Detail view: click Start while paused → reload → state is `running`, ▶ Start highlighted again +- [ ] **3.47** Detail view: streams (stats / logs / processes) stop before pause fires (no reconnect storm in DevTools Network panel) +- [ ] **3.48** Multi-select on list: mix of running + paused → click ▶ Start → running stays running, paused becomes running (per-container dispatch) + +--- + +## 4. Volumes (`/admin/podman/volumes`) + +- [x] **4.1** Load volumes list → Page loads (may be empty) +- [x] **4.2** Click "Create Volume" → Form appears +- [x] **4.3** Submit empty form → Name will be generated +- [x] **4.4** Create volume named `test-vol` → Appears in list with name, driver, created date +- [x] **4.5** Click inspect on `test-vol` → JSON modal shows volume data +- [x] **4.6** Return to overview → Volume count incremented +- [x] **4.7** Select `test-vol` → Delete → Confirmation modal → confirm → removed from list +- [ ] **4.8** Create volume `test-vol-prune` and leave it → Used in prune test (step 8.2) + +--- + +## 5. Networks (`/admin/podman/networks`) + +- [x] **5.1** Load networks list → Default `podman` network visible +- [x] **5.2** Note OpenWrt column for default network → Status icon shown +- [x] **5.3** Click "Create Network" → Form opens +- [x] **5.4** Submit empty form → Validation error +- [x] **5.5** Create `test_net`, subnet `10.220.0.0/24`, gateway `10.220.0.1`, OpenWrt integration checked → Network created; OpenWrt column shows `✓` +- [x] **5.6** Verify UCI config created → SSH: `uci show network.test_net` returns values +- [x] **5.7** Create `test_net_b` with no subnet, integration unchecked → Network created; OpenWrt column shows `-` +- [x] **5.8** If any network shows `X` (incomplete integration): click it → setup → icon changes to `✓` +- [x] **5.9** Return to overview → Network count incremented +- [x] **5.10** Select `test_net` → Delete → Confirmation mentions OpenWrt cleanup → confirm → network removed; UCI config removed +- [x] **5.11** Select `test_net_bare` → Delete → Removed cleanly (no UCI to clean up) + +--- + +## 6. Secrets (`/admin/podman/secrets`) + +- [x] **6.1** Load secrets list → Page loads (may be empty) +- [x] **6.2** Click "Create Secret" → Form with name + value fields +- [x] **6.3** Submit empty form → Validation error +- [x] **6.4** Create secret name `test-secret`, value `hunter2` → Appears in list; value NOT shown anywhere +- [x] **6.5** Click inspect on `test-secret` → Modal shows metadata; `SecretData` field hidden +- [x] **6.6** Delete `test-secret` → Removed from list + +--- + +## 7. Pods (`/admin/podman/pods`) + +*Requires `alpine:latest` from step 2.5. If a user-defined network exists from section 5, note its name as ``.* + +### 7a. List view + +- [x] **7.1** Load pods list → Page loads with empty/populated table; no "Work in progress…" stub +- [x] **7.2** Toolbar visible → Create / Delete / Reload / ▶ / ■ / ⟳ / ⏸ / ⏵ buttons present +- [x] **7.3** Click Reload with empty list → No error, indicator appears briefly + +### 7b. Create - minimal + +- [x] **7.4** Click "Create" → Pod form modal opens with default fields populated +- [x] **7.5** Submit empty form → No validation error (name auto-generates); pod appears with generated name +- [x] **7.6** Create pod name `pod-min`, all other defaults → Modal closes, success notification, `pod-min` row appears +- [x] **7.7** Verify SSH `podman pod inspect pod-min` → Pod exists with one infra container +- [x] **7.8** Containers column shows `1 ▸` for `pod-min` + +### 7c. Create - full options + +- [x] **7.9** Open Create form → Name `pod-full`, Hostname `pod-full-host`, Shared Namespaces `cgroup,ipc,net,uts`, Infra ON, Network ``, DNS Servers `1.1.1.1` and `9.9.9.9`, DNS Search `lan`, CPU Limit `1.0`, CPU Set `0-1`, Memory `256m`, Labels `app=demo\nenv=test` → Submit +- [x] **7.10** SSH `podman pod inspect pod-full | jq '.Hostname'` → `pod-full-host` +- [x] **7.11** SSH `podman pod inspect pod-full | jq '.InfraConfig.NetworkOptions'` → contains DNS server + search entries +- [x] **7.12** SSH `podman pod inspect pod-full | jq '.Labels'` → `app: demo`, `env: test` +- [x] **7.13** Networks column for `pod-full` → shows `` + +### 7d. Status badges + +- [x] **7.14** New pod → Status badge `Created` (gray) +- [x] **7.15** After Start → Badge `Running` (green) +- [x] **7.16** After Pause → Badge `Paused` (orange) +- [x] **7.17** After Stop → Badge `Exited` or `Stopped` (red) + +### 7e. Lifecycle - single select + +- [x] **7.18** Tick `pod-min`, click ▶ Start → Loading "Starting pod: 1/1", refresh shows `Running` +- [x] **7.19** Click ■ Stop → Status `Exited`/`Stopped` +- [x] **7.20** Click ⟳ Restart → Status `Running` +- [x] **7.21** Click ⏸ Pause → Status `Paused` +- [x] **7.22** Click ⏵ Unpause → Status `Running` + +### 7f. Lifecycle - multi-select + +- [ ] **7.23** Create `pod-a` and `pod-b` (defaults), select both → ▶ Start +- [ ] **7.24** Loading modal shows "Starting pod: 1/2" then "2/2" +- [ ] **7.25** Both pods end up `Running` +- [ ] **7.26** No JS console errors during multi-action + +### 7g. Detail page navigation (from list) + +- [ ] **7.27** Click pod **Name** in list → Navigates to `/admin/podman/pod/` (no more inspect modal) +- [ ] **7.28** Browser back button → Returns to pods list with state preserved + +### 7h. Containers expand modal + +- [x] **7.29** SSH `podman run -d --pod pod-min --name pod-min-c1 alpine sleep 3600` → container created in pod +- [x] **7.30** Reload pods list → `pod-min` Containers column shows `2 ▸` +- [x] **7.31** Click `2 ▸` link → Modal "Containers in pod pod-min" opens with 2 rows (infra + new) +- [x] **7.32** Each row shows Name, truncated ID, Status badge, Restarts count +- [x] **7.33** Click container ID link in modal → Navigates to `/admin/podman/container/` detail view + +### 7i. Pod removal + +- [ ] **7.34** Stop `pod-min` first → Status `Exited` +- [ ] **7.35** Tick `pod-min` → Delete → Confirm +- [ ] **7.36** SSH `podman pod ls` → `pod-min` gone +- [ ] **7.37** SSH `podman ps -a` → `pod-min-c1` also gone (cascade delete via force) + +### 7j. Container form integration + +- [x] **7.38** Navigate **Podman → Containers → Create** → Form shows new **Pod** ListValue (after Image) with "(none)" + existing pod names +- [x] **7.39** Select Pod = `pod-full` → Network, Hostname, Port Mappings, Expose Ports fields hide +- [x] **7.40** Select Pod = "(none)" → Hidden fields reappear +- [x] **7.41** Create container with Pod `pod-full`, Image `alpine:latest`, Command `sleep 3600`, Start ON → Success +- [x] **7.42** SSH `podman ps --filter pod=pod-full` → New container listed +- [x] **7.43** Pods list `pod-full` Containers column → count incremented + +### 7k. Edge cases + +- [ ] **7.44** Create pod with name `Bad Name!` → Validation error (uciname constraint) +- [ ] **7.45** Toggle "Create Infra Container" OFF → Network/DNS/Infra Image fields hide +- [ ] **7.46** Submit pod with Infra OFF → Pod created without infra container; SSH `podman pod inspect` confirms +- [ ] **7.47** Try Stop on already-stopped pod → No JS error (graceful handling of API 304/409) +- [ ] **7.48** Try Pause on stopped pod → Error notification, no JS crash + +### 7l. Regression - non-pod container create + +- [ ] **7.49** Create container with Pod = "(none)", port mapping `8080:80`, network ``, hostname `freebee` → Success +- [ ] **7.50** SSH inspect → port mapping, network and hostname present (confirms `if (!data.pod)` guards don't break the non-pod path) + +### 7m. Cleanup + +- [ ] **7.51** SSH `podman pod rm -f pod-full pod-a pod-b` → All test pods removed +- [ ] **7.52** SSH `podman ps -a` → No leftover test containers +- [ ] **7.53** Reload pods list → Empty/clean state + +### 7n. Detail page - load & header (`/admin/podman/pod/`) + +- [ ] **7.54** Direct URL `/admin/podman/pod/` → Detail page loads, no console errors +- [ ] **7.55** Header shows pod name and back-arrow button → back button returns to pods list +- [ ] **7.56** Lifecycle buttons visible: ▶ Start, ■ Stop, ⟳ Restart, ⏸ Pause, ⏵ Unpause, 🗑 Delete +- [ ] **7.57** "Active" highlight reflects current state: Running pod → ▶ highlighted, Paused pod → ⏸ highlighted, Exited pod → ■ highlighted (semantic: button describes current state, not "available action") +- [ ] **7.58** Tabs visible: Info, Stats, Processes, Inspect + +### 7o. Detail page - Info tab + +- [ ] **7.59** Basic Information section: Name, ID (full 64-char), Status badge, Created date populated +- [ ] **7.60** Configuration section: Hostname, Cgroup parent, Exit policy, Restart policy, Shared namespaces (comma-joined), Infra container (clickable link to `/admin/podman/container/`) +- [ ] **7.61** Resources section: CPU period (µs), CPU quota (µs or "Unlimited"), CPU shares, CPU set, Memory limit (formatted bytes or "Unlimited"). Description note "Resources are set at pod creation and cannot be edited here." present +- [ ] **7.62** Network section: Networks, Static IPv4, Static MAC, DNS servers, DNS search, DNS options, Extra hosts, Host network (Yes/No) +- [ ] **7.63** Containers section: table with Name, truncated ID (clickable → container detail), Status badge - one row per container including infra +- [ ] **7.64** Labels section: only rendered if pod has labels; otherwise omitted +- [ ] **7.65** All empty fields show `-` placeholder + +### 7p. Detail page - Stats tab (pod running) + +- [ ] **7.66** Switch to Stats tab → Table with headers: Container, CID, CPU %, Memory, Memory %, Net I/O, Block I/O, PIDs +- [ ] **7.67** Rows update live (humanized strings from `PodStatsReport`: e.g. "75.5%", "12mb / 24mb") +- [ ] **7.68** Container name column resolves from pod's containers map (not the pod-name value the API returns) +- [ ] **7.69** Switch to another tab → Stream stops (verify in DevTools Network: stream connection closes) +- [ ] **7.70** Switch back to Stats → Stream restarts +- [ ] **7.71** Pod is stopped → Stats tab shows "Pod is not running" warning, no stream attempt + +### 7q. Detail page - Processes tab (pod running) + +- [ ] **7.72** Switch to Processes tab → Table with columns Podman's `pod top` returns (USER, PID, ..., CMD) plus a CONTAINER column (native to pod top) +- [ ] **7.73** Rows update live as processes change +- [ ] **7.74** Switch away → Stream stops +- [ ] **7.75** Pod is stopped → Shows "Pod is not running" warning + +### 7r. Detail page - Inspect tab + +- [ ] **7.76** Switch to Inspect tab → Renders full raw JSON of `podman pod inspect` +- [ ] **7.77** Same content as old list-view inspect modal (sanity check no fields are lost) + +### 7s. Detail page - lifecycle actions + +- [ ] **7.78** Running pod → click ⏸ Pause → confirmation/loading → reload → status `Paused`, ⏸ now highlighted as active +- [ ] **7.79** Paused pod → click ⏵ Unpause → reload → status `Running` +- [ ] **7.80** Running pod → click ■ Stop → reload → status `Exited`, ■ highlighted +- [ ] **7.81** Stopped pod → click ▶ Start → reload → status `Running` +- [ ] **7.82** Click ⟳ Restart → reload → status briefly cycles to Running +- [ ] **7.83** Click 🗑 Delete → confirm dialog → confirm → redirects to `/admin/podman/pods`, pod gone +- [ ] **7.84** Before Stop/Pause/Restart, verify in DevTools Network: stats/processes streams close BEFORE the action fires (no reconnect storm) + +### 7t. Detail page - edge cases + +- [ ] **7.85** Pod with no containers (rare - infra-less or just created): Containers section shows "No containers in this pod" +- [ ] **7.86** Pod with no labels: Labels section is absent (not just empty) +- [ ] **7.87** Pod with `RestartPolicy` unset: Configuration shows `-` +- [ ] **7.88** Pod with `memory_limit: 0` or unset: Resources shows "Unlimited" +- [ ] **7.89** Pod with `cpu_quota: -1`: Resources shows "Unlimited" +- [ ] **7.90** Invalid pod ID in URL → Redirects to pods list (no crash) +- [ ] **7.91** Refresh page while stats stream active → No duplicate streams (Network panel shows single connection) + +--- + +## 8. Modals + +### Prune Modal + +- [x] **8.1** Open prune modal → Options shown: containers, images, volumes, networks +- [ ] **8.2** Select only "Volumes" → Prune → Only unused volumes pruned (e.g. `test-vol-prune` from step 4.8); containers/images untouched +- [ ] **8.3** Select "Containers" → Prune → Only stopped/exited containers removed + +### Auto-Update Modal + +- [x] **8.4** Open auto-update modal → Container list shown with current digest info +- [x] **8.5** Click "Check for Updates" → Digest comparison runs; up-to-date containers marked +- [x] **8.6** If outdated container detected → Update → Pulls new image, recreates container + +--- + +## 9. Error Handling + +- [ ] **9.1** Stop Podman socket (`/etc/init.d/podman stop`), load any list view → Clear RPC error message; no JS crash, no blank page +- [x] **9.2** Navigate to `/admin/podman/container/nonexistent-id` → Graceful error, not a blank page +- [ ] **9.3** Start Podman again (`/etc/init.d/podman start`); reload → Normal operation resumes +- [x] **9.4** Trigger two operations quickly in succession → No race-condition crash or duplicate requests + +--- + +## 10. Cleanup Verification + +- [x] **10.1** Delete any remaining test containers (e.g. `test-alpine-renamed`) +- [x] **10.2** Delete `alpine` image +- [x] **10.3** Containers list → Empty (or back to pre-test state) +- [x] **10.4** Images list → Empty (or back to pre-test state) +- [x] **10.5** Overview counters → Match baseline from step 1.4 diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/TableSelectSection.js b/luci-app-podman/htdocs/luci-static/resources/podman/TableSelectSection.js new file mode 100644 index 000000000..9e8580998 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/TableSelectSection.js @@ -0,0 +1,326 @@ +'use strict'; + +'require baseclass'; +'require dom'; +'require form'; +'require ui'; +'require podman.form as podmanForm'; +'require podman.ui as podmanUI'; +'require podman.utils as podmanUtil'; + +const FormTableSelectSection = form.TableSection.extend({ + __name__: 'Podman.Form.TableSelectSection', + + checkboxColumn: null, + view: null, + toolbarExtraButtons: [], + modalCreateBtnText: null, + modalCreateTitle: null, + handleRefreshCb: null, + + render() { + if (!this.checkboxColumn) { + this.checkboxColumn = this.option( + podmanForm.field.SelectDummyValue, + this.selectKey, + new ui.Checkbox(0, { hiddenname: 'all' }).render() + ); + this.checkboxColumn.width = 30; + + podmanUtil.moveArrayItem(this.children, this.children.length - 1, 0); + } + + return this.super('render', []); + }, + + renderContents(cfgsections, nodes) { + const config_name = this.uciconfig ?? this.map.config; + const max_cols = this.children.length; + + if (this.footer === true) { + this.footer = [ + `${_('Total')} ${nodes.length}`, + ]; + } + + const sectionEl = E('div', { + id: 'cbi-%s-%s'.format(config_name, this.sectiontype), + class: 'cbi-section cbi-tblsection', + }); + + const tableEl = E('table', { class: 'table cbi-section-table cbi-section-table-select' }); + const theadEl = E('thead', { class: 'thead cbi-section-thead' }); + const tbodyEl = E('tbody', { class: 'tbody cbi-section-tbody' }); + const tfootEl = E('tfoot', { class: 'tfoot cbi-section-tfoot' }); + + if (this.description != null && this.description !== '') { + sectionEl.appendChild(E('div', { class: 'cbi-section-descr' }, this.description)); + } + + sectionEl.appendChild(E('div', { class: 'cbi-section-actions' }, this.renderToolbar())); + + theadEl.appendChild(this.renderHeaderRows(false)); + + if (theadEl.hasChildNodes()) + tableEl.appendChild(theadEl); + + for (let i = 0; i < nodes.length; i++) { + const trEl = E('tr', { + id: 'cbi-%s-%s'.format(config_name, cfgsections[i]), + class: 'tr cbi-section-table-row', + 'data-sid': cfgsections[i], + 'data-section-id': cfgsections[i] + }); + + for (let j = 0; j < max_cols && nodes[i].firstChild; j++) { + trEl.appendChild(nodes[i].firstChild); + } + + tbodyEl.appendChild(trEl); + } + + if (nodes.length === 0) + tbodyEl.appendChild(E('tr', { class: 'tr cbi-section-table-row' }, + E('td', { class: 'td text-center', colspan: max_cols }, this.renderSectionPlaceholder()))); + + tableEl.appendChild(tbodyEl); + + tfootEl.appendChild(this.renderFooterRows(false)); + + if (tfootEl.hasChildNodes()) { + tableEl.appendChild(tfootEl); + } + + this.registerCheckboxEvents(theadEl, tableEl); + + sectionEl.appendChild(tableEl); + + dom.bindClassInstance(sectionEl, this); + + return sectionEl; + }, + + renderToolbar() { + return E('div', { class: 'list-toolbar mb-sm d-flex justify-center gap-xs' }, this.getToolbarButtons()); + }, + + getToolbarButtons() { + const buttons = []; + + if (this.handleCreate) { + buttons.push(this.getCreateButton()); + } + if (this.handleRemove) { + buttons.push(this.getRemoveButton()); + } + + if (this.handleRefresh) { + buttons.push(this.getReloadButton()); + } + + if (this.toolbarExtraButtons.length > 0) { + buttons.push(...this.toolbarExtraButtons); + } + + return buttons; + }, + + getCreateButton() { + return new podmanUI.ButtonNew(_('Create'), { + click: ui.createHandlerFn(this, 'handleCreate'), + type: 'add', + }).render(); + }, + + getRemoveButton() { + return new podmanUI.ButtonNew(_('Delete'), { + click: ui.createHandlerFn(this, 'handleRemove'), + type: 'remove', + }).render(); + }, + + getReloadButton() { + return new podmanUI.ButtonNew(_('Reload'), { + click: ui.createHandlerFn(this, 'handleRefresh'), + type: 'apply', + }).render(); + }, + + async handleCreate() { + const createForm = new this.createForm.init(); + const formElement = await createForm.render(); + const title = this.modalCreateTitle || _('Create %s').format(this.view.titleSingle || '').trim(); + + const modal = new podmanUI.Modal(title, [ formElement ]); + modal.getButtons = () => [ + modal.getCloseButton(), + new podmanUI.ButtonNew(this.modalCreateBtnText || _('Create'), { + click: () => { + createForm.handleCreate().then(() => this.handleRefresh()); + }, + type: 'positive', + }).render(), + ]; + modal.render(); + }, + + async handleRemove() { + const selected = this.getSelectedData(); + + if (selected.length === 0) { + return this.showNoneSelectedWarning(); + } + + this.view.confirm([ + E('p', {}, _('Are you sure to delete the records?')), + ], async () => { + let i = 1; + for (const item of selected) { + this.view.loading(_('Deleting records: %d/%d').format(i, selected.length)); + + await item.remove(); + + i++; + } + + ui.hideModal(); + this.handleRefresh(); + }); + }, + + async handleRefresh() { + const indicatorId = 'podman-refresh-' + this.sectiontype; + ui.showIndicator(indicatorId, _('Refreshing %s...').format(this.sectiontype)); + + const data = await this.view.load(); + + if (this.footer) { + const count = Array.isArray(data) ? data.length : 0; + this.footer = [`${_('Total')} ${count}`]; + } + + const obj = {}; + obj[this.sectiontype] = data; + + this.map.data.__init__(obj); + + this.map.root + .querySelector('input[type="hidden"][name="all"] ~ input[type=checkbox]').checked = false; + + await this.map.save(null, false); + await this.map.load(); + await this.map.reset(); + + if (this.handleRefreshCb) await this.handleRefreshCb(); + + ui.hideIndicator(indicatorId); + }, + + handleInspect(item, hiddenFields) { + this.view.loading(_('Fetching information...')); + + item.inspect().then((data) => { + ui.hideModal(); + this.showInspectModal(data, hiddenFields); + }); + }, + + showInspectModal(data, hiddenFields) { + const displayData = JSON.parse(JSON.stringify(data)); + + if (hiddenFields && hiddenFields.length > 0) { + hiddenFields.forEach((field) => { + if (displayData[field]) { + displayData[field] = '***HIDDEN***'; + } + }); + } + + const content = [ + new podmanUI.JsonArea(displayData).render(), + ]; + + if (hiddenFields && hiddenFields.length > 0) { + content.unshift(E('p', { class: 'mb-sm text-error' }, [ + E('strong', {}, _('Security Notice:')), ' ', _('Sensitive data is hidden for security reasons.') + ])); + } + + const modal = new podmanUI.Modal('', content); + modal.handleSubmit = undefined; + modal.render(); + }, + + showNoneSelectedWarning() { + this.view.warning(_('No %s selected').format(this.sectiontype)); + }, + + registerCheckboxEvents(theadEl, tableEl) { + const checkboxSelectAll = theadEl.querySelector('input[name="all"] + input[type="checkbox"]'); + const rows = tableEl.querySelectorAll('tbody tr'); + let lastClickedIndex = -1; + + if (!checkboxSelectAll) { + return; + } + + checkboxSelectAll.addEventListener('click', (event) => { + const checked = event.target.checked; + tableEl.querySelectorAll('tbody tr input[type="checkbox"]').forEach((checkbox) => { + checkbox.checked = checked; + }); + }); + + rows.forEach((row, index) => { + const checkbox = row.querySelector('input[type=checkbox]'); + + if (!checkbox) { + return; + } + + row.addEventListener('click', (ev) => { + const clickedInteractiveElement = ev.target.closest('button, a, input, .cbi-tooltip-container'); + + // If the click was inside an interactive element, ignore it and exit. + if (clickedInteractiveElement) { + return; + } + + checkbox.click(); + }); + + checkbox.addEventListener('click', (ev) => { + // Handle shift+click for range selection + if (ev.shiftKey && lastClickedIndex !== -1 && + lastClickedIndex !== index) { + const start = Math.min(lastClickedIndex, index); + const end = Math.max(lastClickedIndex, index); + const targetState = checkbox.checked; + + // Select/deselect all checkboxes in range + for (let i = start; i <= end; i++) { + rows[i].querySelector('input[type=checkbox]').checked = targetState; + } + } + + // Update last clicked index + lastClickedIndex = index; + }); + }); + }, + + getSelectedCheckboxes() { + return this.map.root.querySelectorAll('tbody tr input[type=checkbox]:checked'); + }, + + getSelectedData() { + return [...this.getSelectedCheckboxes()].map((checkbox) => { + const sectionId = checkbox.previousSibling.name; + return this.map.data.data[sectionId]; + }); + } +}); + +return baseclass.extend({ + TableSelectSection: FormTableSelectSection +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/constants.js b/luci-app-podman/htdocs/luci-static/resources/podman/constants.js new file mode 100644 index 000000000..663fad09b --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/constants.js @@ -0,0 +1,22 @@ +'use strict'; + +'require baseclass'; + +const NOTIFICATION_TIMEOUT = 2000; + +const ICON = { + START: '►', + STOP: '◼', + RESTART: '↻', + PAUSE: '❚❚', + CLEAR_LOG: '🗑️', + INIT_ENABLED: '✓', + INIT_MISSING: '⚠', + INIT_DISABLED: '⏼', + BACK: '🔙', +}; + +return baseclass.extend({ + NOTIFICATION_TIMEOUT, + ICON, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form.js b/luci-app-podman/htdocs/luci-static/resources/podman/form.js new file mode 100644 index 000000000..6e56e2ae9 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form.js @@ -0,0 +1,156 @@ +'use strict'; + +'require baseclass'; +'require ui'; +'require form'; + +'require podman.ui as podmanUI'; +'require podman.utils as podmanUtil'; + +const FormMemoryValue = form.Value.extend({ + placeholder: '512m, 1g, -1', + validate(_section_id, value) { + if (!value) return true; + if (value === '-1' || value === '0') return true; + if (!/^\d+(?:\.\d+)?\s*[kmg]b?$/i.test(value)) { + return _('Expecting: %s').format(_('512m, 1g, or -1 for unlimited')); + } + return true; + }, +}); + +/** + * + */ +const FormDummyValue = form.DummyValue.extend({ + __name__: 'Podman.Form.DummyValue', + + cfgformatter: (cfg, _data) => cfg, + cfgdatavalue: (_data) => '', + cfgtt: (_cfg, _data) => '', + + cfgvalue(section_id, set_value) { + if (section_id == null) + L.error('TypeError', 'Section ID required'); + + const data = this.map.data.data[section_id]; + const dataValue = this.cfgdatavalue(data); + + if (dataValue) { + return this._renderValue(dataValue, data); + } + + if (arguments.length === 2 && set_value) { + this.data ??= {}; + this.data[section_id] = set_value; + } + + return this._renderValue(this.data?.[section_id], data); + }, + + _renderValue(value, data) { + const tooltip = this.cfgtt(value, data); + const formatted = this.cfgformatter(value, data); + return tooltip ? new podmanUI.Tooltip(formatted, tooltip).render() : formatted; + }, +}); + +/** + * + */ +const FormSelectDummyValue = form.DummyValue.extend({ + __name__: 'Podman.Form.SelectDummyValue', + + cfgvalue(sectionId) { + return new ui.Checkbox(0, { hiddenname: sectionId }).render(); + } +}); + +/** + * + */ +const FormLinkDummyValue = FormDummyValue.extend({ + __name__: 'Podman.Form.LinkDummyValue', + + url: '#', + click: undefined, + + cfgformatter(cfg, model) { + return E('a', { + class: 'text-bold', + href: this.url, + click: (ev) => { + if (typeof this.click === 'function') { + ev.preventDefault(); + this.click(cfg, model); + } + } + }, cfg); + } +}); + +/** + * + */ +const FormDateDummyValue = FormDummyValue.extend({ + __name__: 'Podman.Form.DateDummyValue', + cfgformatter: podmanUtil.format.date, +}); + +/** + * + */ +const FormTimestampDummyValue = FormDateDummyValue.extend({ + __name__: 'Podman.Form.TimestampDummyValue', + cfgformatter: (value) => podmanUtil.format.date(parseInt(value)), +}); + +/** + * + */ +const FormByteDummyValue = FormDummyValue.extend({ + __name__: 'Podman.Form.ByteDummyValue', + cfgformatter: podmanUtil.format.bytes, +}); + +const FormEditableField = baseclass.extend({ + __name__: 'Podman.Form.EditableField', + name: null, + formField: null, + + __init__(name, formField, onSubmit) { + this.name = name; + this.formField = formField; + if (onSubmit) { + this.onSubmit = onSubmit; + } + }, + + render() { + return E('div', { class: `editable-field editable-field-${this.name}` }, [ + this.formField, + new podmanUI.Button( + _('Update'), + () => this.onSubmit(document.querySelector(`.editable-field-${this.name} [name="${this.name}"]`).value), + 'apply' + ).render() + ]); + }, + + onSubmit(_value) {} +}); + +return baseclass.extend({ + __name__: 'Podman.Form', + + EditableField: FormEditableField, + field: { + MemoryValue: FormMemoryValue, + DummyValue: FormDummyValue, + ByteDummyValue: FormByteDummyValue, + DateDummyValue: FormDateDummyValue, + LinkDummyValue: FormLinkDummyValue, + SelectDummyValue: FormSelectDummyValue, + TimestampDummyValue: FormTimestampDummyValue, + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/container.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/container.js new file mode 100644 index 000000000..406bf4764 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/container.js @@ -0,0 +1,337 @@ +'use strict'; + +'require baseclass'; +'require form'; + +'require podman.form as podmanForm'; +'require podman.utils as podmanUtil'; +'require podman.rpc as podmanRPC'; +'require podman.view as podmanView'; +'require podman.model.Container as Container'; + +/** + * Create podman container + */ +const PodmanFormContainer = podmanView.form.extend({ + __name__: 'Podman.Form.Container', + + makeData() { + return { + container: { + name: '', + image: '', + pod: '', + command: null, + ports: null, + env: null, + volumes: null, + network: 'bridge', + restart: 'no', + privileged: '0', + tty: '0', + remove: '0', + autoupdate: '0', + start: '0', + workdir: null, + hostname: null, + user: null, + groups: null, + expose: null, + labels: null, + cpus: null, + memory: null, + } + }; + }, + + async createForm() { + const [images, networks, pods] = await Promise.all([ + podmanRPC.images.list(), + podmanRPC.networks.list(), + podmanRPC.pods.list() + ]); + + let field; + field = this.section.option(form.Value, 'name', _('Container Name')); + field.placeholder = 'my-container'; + field.optional = true; + field.datatype = 'maxlength(253)'; + field.description = _('Leave empty to auto-generate'); + + field = this.section.option(form.ListValue, 'image', _('Image')); + field.value('', _('-- Select %s --').format(_('Image'))); + images.forEach((img) => { + img.getRepoTags().forEach((tag) => { + if (tag !== ':') { + field.value(tag, tag); + } + }); + }); + field.rmempty = false; + field.optional = false; + field.validate = (_section_id, value) => { + if (!value) { + return _('Expecting: %s').format(_('non-empty value')); + } + return true; + }; + field.description = _('Container image to use'); + + field = this.section.option(form.ListValue, 'pod', _('Pod')); + field.value('', _('(none)')); + pods.forEach((pod) => { + const name = pod.getName(); + if (name && name !== _('Unknown')) { + field.value(name, name); + } + }); + field.description = _('Add the container to an existing pod. Network, hostname and port mappings come from the pod\'s infra container.'); + + field = this.section.option(form.Value, 'command', _('Command')); + field.placeholder = '/bin/sh'; + field.optional = true; + field.description = _('Command to run (space-separated)'); + + field = this.section.option(form.TextValue, 'ports', _('Port Mappings')); + field.depends('pod', ''); + field.placeholder = '8080:80\n8443:443'; + field.rows = 3; + field.optional = true; + field.description = _('One per line, format: host:container[/protocol]'); + + field = this.section.option(form.Value, 'expose', _('Expose Ports')); + field.depends('pod', ''); + field.placeholder = '6052, 8080/udp'; + field.optional = true; + field.description = _('Comma-separated ports to expose (e.g., 6052, 8080/udp)'); + + field = this.section.option(form.TextValue, 'env', _('Environment Variables')); + field.placeholder = 'VAR1=value1\nVAR2=value2'; + field.rows = 4; + field.optional = true; + field.description = _('One per line, format: key=value'); + + field = this.section.option(form.TextValue, 'volumes', _('Volumes')); + field.placeholder = '/host/path:/container/path:ro\nvolume-name:/data'; + field.rows = 4; + field.optional = true; + field.description = _('One per line. Format: source:destination[:options]. Options: ro, rw, Z, z'); + + field = this.section.option(form.ListValue, 'network', _('Network')); + field.depends('pod', ''); + field.value('bridge', 'bridge (default)'); + field.value('host', 'host'); + field.value('none', 'none'); + networks.forEach((net) => { + const name = net.getName(); + if (name && name !== 'bridge' && name !== 'host' && name !== 'none') { + field.value(name, name); + } + }); + field.rmempty = false; + field.description = _( + 'Select network for the new container. User-created networks provide better isolation and DNS resolution between containers.' + ); + + field = this.section.option(form.ListValue, 'restart', _('Restart Policy')); + field.value('no', _('No')); + field.value('always', _('Always')); + field.value('on-failure', _('On Failure')); + field.value('unless-stopped', _('Unless Stopped')); + + field = this.section.option(form.Flag, 'privileged', _('Privileged Mode')); + + field = this.section.option(form.Flag, 'tty', _('Allocate TTY (-t)')); + + field = this.section.option(form.Flag, 'remove', _('Auto Remove (--rm)')); + + field = this.section.option(form.Flag, 'autoupdate', _('Auto-Update')); + field.description = _( + 'Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry' + ); + + field = this.section.option(form.Flag, 'start', _('Start after creation')); + field.description = _('Automatically start the container after it is created'); + + field = this.section.option(form.Value, 'workdir', _('Working Directory')); + field.placeholder = '/app'; + field.optional = true; + + field = this.section.option(form.Value, 'hostname', _('Hostname')); + field.depends('pod', ''); + field.placeholder = 'container-host'; + field.optional = true; + field.datatype = 'hostname'; + + field = this.section.option(form.Value, 'user', _('User')); + field.placeholder = '1000:1000'; + field.optional = true; + field.description = _('User and group to run as (UID:GID)'); + + field = this.section.option(form.Value, 'groups', _('Supplementary Groups')); + field.placeholder = '500,1000'; + field.optional = true; + field.description = _('Comma-separated list of supplementary group IDs'); + + field = this.section.option(form.TextValue, 'labels', _('Labels')); + field.placeholder = 'key1=value1\nkey2=value2'; + field.rows = 3; + field.optional = true; + field.description = _('One per line, format: key=value'); + + field = this.section.option(form.Value, 'cpus', _('CPU Limit')); + field.placeholder = '1.0'; + field.optional = true; + field.datatype = 'ufloat'; + field.description = _('Number of CPUs (e.g., 0.5, 1.0, 2.0)'); + + field = this.section.option(podmanForm.field.MemoryValue, 'memory', _('Memory Limit')); + field.optional = true; + field.description = _('Memory limit (e.g., 512m, 1g)'); + }, + + async handleCreate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + + const data = this.getFieldValues(); + const spec = { + image: data.image, + privileged: Boolean(parseInt(data.privileged)), + terminal: Boolean(parseInt(data.tty)), + remove: Boolean(parseInt(data.remove)), + }; + + if (data.name) spec.name = data.name; + if (data.pod) spec.pod = data.pod; + if (data.command) spec.command = data.command.split(/\s+/).filter((c) => c.length > 0); + + if (!data.pod && data.ports) { + spec.portmappings = []; + data.ports.split('\n').forEach((line) => { + const trimmed = line.trim(); + if (!trimmed) return; + const parts = trimmed.split(':'); + if (parts.length === 2) { + const hostPort = parseInt(parts[0], 10); + const [containerPortStr, proto] = parts[1].split('/'); + const containerPort = parseInt(containerPortStr, 10); + if (!isNaN(hostPort) && !isNaN(containerPort)) { + spec.portmappings.push({ + host_port: hostPort, + container_port: containerPort, + protocol: (proto || 'tcp').toLowerCase() + }); + } + } + }); + } + + const parseKeyValue = (text, target) => { + text.split('\n').forEach((line) => { + const parts = line.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + if (key) target[key] = value; + } + }); + }; + + if (data.env) { + spec.env = {}; + parseKeyValue(data.env, spec.env); + } + if (data.volumes) { + const mounts = []; + const volumes = []; + data.volumes.split('\n').forEach((line) => { + const parts = line.trim().split(':'); + if (parts.length < 2) return; + + const opts = parts.length > 2 ? parts[2].split(',') : []; + if (parts[0].includes('/')) { + const mount = { Source: parts[0], Destination: parts[1] }; + if (opts.includes('ro')) mount.ReadOnly = true; + const selinux = opts.filter((o) => o === 'Z' || o === 'z'); + if (selinux.length > 0) mount.options = selinux; + mounts.push(mount); + return; + } + + const vol = { Name: parts[0], Dest: parts[1] }; + if (opts.length > 0) vol.Options = opts; + volumes.push(vol); + }); + if (mounts.length > 0) spec.mounts = mounts; + if (volumes.length > 0) spec.volumes = volumes; + } + if (!data.pod) { + if (data.network === 'host') { + spec.netns = { nsmode: 'host' }; + } else if (data.network === 'none') { + spec.netns = { nsmode: 'none' }; + } else if (data.network && data.network !== 'bridge') { + spec.networks = { [data.network]: {} }; + } + } + if (data.restart !== 'no') spec.restart_policy = data.restart; + if (data.workdir) spec.work_dir = data.workdir; + if (!data.pod && data.hostname) spec.hostname = data.hostname; + if (data.user) spec.user = data.user; + if (data.groups) { + spec.groups = data.groups.split(',').map((g) => g.trim()).filter((g) => g); + } + if (!data.pod && data.expose) { + spec.expose = {}; + data.expose.split(',').forEach((p) => { + const trimmed = p.trim(); + if (!trimmed) return; + const parts = trimmed.split('/'); + const port = parseInt(parts[0], 10); + if (port >= 1 && port <= 65535) + spec.expose[String(port)] = (parts[1] || 'tcp').toLowerCase(); + }); + } + if (data.labels || data.autoupdate === '1') { + spec.labels = {}; + if (data.labels) parseKeyValue(data.labels, spec.labels); + if (data.autoupdate === '1') spec.labels['io.containers.autoupdate'] = 'registry'; + } + if (data.cpus || data.memory) { + spec.resource_limits = {}; + if (data.cpus) { + spec.resource_limits.cpu = { quota: parseFloat(data.cpus) * 100000 }; + } + if (data.memory) { + const memBytes = podmanUtil.format.parseMemory(data.memory); + if (memBytes > 0) spec.resource_limits.memory = { limit: memBytes }; + } + } + + const hasRestartPolicy = data.restart && data.restart !== 'no'; + const shouldStart = Boolean(parseInt(data.start)); + + const createFn = async () => { + const raw = await podmanRPC.containers.create(spec); + const inspected = await Container.getSingleton(raw).inspect(); + const container = Container.getSingleton(inspected); + + if (hasRestartPolicy) { + await container.generateInitScript(); + await container.enableInitScript(); + } + + if (shouldStart) await container.start(); + }; + + return this.super('handleCreate', [ createFn, _('Container') ]); + }, +}); + +return baseclass.extend({ + init: PodmanFormContainer +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/image.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/image.js new file mode 100644 index 000000000..fa48445e9 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/image.js @@ -0,0 +1,91 @@ +'use strict'; + +'require baseclass'; +'require form'; +'require ui'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.model.Image as Image'; + +/** + * Pull podman image + */ +const PodmanFormImage = podmanView.form.extend({ + __name__: 'Podman.Form.Image', + + makeData() { + return { + image: { + registry: '', + image: '', + } + }; + }, + + createForm() { + let field; + + field = this.section.option(form.ListValue, 'registry', _('Registry')); + field.value('', 'docker.io'); + field.value('quay.io/', 'quay.io'); + field.value('ghcr.io/', 'ghcr.io'); + field.value('gcr.io/', 'gcr.io'); + + field = this.section.option(form.Value, 'image', _('Image')); + field.placeholder = 'nginx:latest'; + field.rmempty = false; + }, + + async handleCreate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + + const registry = this.getFieldValue('registry'); + const image = this.getFieldValue('image'); + + const imageName = this.buildImageName(registry, image); + const imageModel = Image.getSingleton({ RepoTags: [imageName] }); + + const progressEl = E('pre', { class: 'terminal-area' }); + const modal = new podmanUI.Modal(_('Pulling %s').format(imageName), [ progressEl ]); + + /** @type {HTMLElement} */ + const closeButton = modal.getCloseButton(); + closeButton.classList.add('d-none'); + + modal.getButtons = () => [ closeButton ]; + modal.render(); + + const append = (text) => { + progressEl.appendChild(document.createTextNode(text)); + progressEl.scrollTop = progressEl.scrollHeight; + }; + + try { + await imageModel.streamPull(append); + closeButton.classList.remove('d-none'); + podmanUI.alert(_('Image pulled successfully'), 'success', true); + } catch (error) { + this.error(error); + } + }, + + buildImageName(registry, image) { + const reg = (registry || '').trim(); + const img = (image || '').trim(); + + if (!reg) { + return img.includes('/') ? 'docker.io/' + img : 'docker.io/library/' + img; + } + + return reg.replace(/\/+$/, '') + '/' + img.replace(/^\/+/, ''); + }, +}); + +return baseclass.extend({ + init: PodmanFormImage +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/network.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/network.js new file mode 100644 index 000000000..7828b2f75 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/network.js @@ -0,0 +1,304 @@ +'use strict'; + +'require baseclass'; +'require form'; +'require ui'; +'require uci'; +'require network'; +'require firewall'; + +'require podman.utils as utils'; +'require podman.rpc as podmanRPC'; +'require podman.view as podmanView'; +'require podman.model.Network as Network'; + +/** + * Create podman network + */ +const PodmanFormNetwork = podmanView.form.extend({ + __name__: 'Podman.Form.Network', + + existingZones: null, + parentDevices: null, + + makeData() { + return { + network: { + name: null, + driver: 'bridge', + subnet: null, + gateway: null, + ip_range: null, + ipv6: '0', + internal: '0', + labels: null, + setup_openwrt: '1', + firewall_zone: '_create_new_', + } + }; + }, + + async render() { + const [zones, parentDevices] = await Promise.all([ + firewall.getZones().then((zones) => + zones + .filter(zone => zone.data.name?.startsWith('podman')) + .map(zone => zone.data.name) + ), + network.getDevices().then((devices) => + devices.filter(this.isValidParentDevice) + ), + ]); + + this.existingZones = zones; + this.parentDevices = parentDevices; + + return this.super('render', []); + }, + + createForm: function () { + let field; + + field = this.section.option(form.Value, 'name', _('Network Name')); + field.placeholder = 'my-network'; + field.datatype = 'and(uciname,maxlength(15))'; + field.description = _('Name for the network'); + field.rmempty = false; + + field = this.section.option(form.ListValue, 'driver', _('Driver')); + field.value('bridge', 'bridge'); + field.value('macvlan', 'macvlan'); + field.value('ipvlan', 'ipvlan'); + field.description = _('Network driver'); + + field = this.section.option(form.ListValue, 'parent', _('Parent Interface')); + field.depends('driver', 'macvlan'); + field.depends('driver', 'ipvlan'); + field.description = _( + 'Existing physical interface to use as parent. Required for macvlan/ipvlan networks.' + ); + if (this.parentDevices.length > 0) { + this.parentDevices.forEach((device) => { + const name = device.getName(); + field.value(name, name + ' (' + device.getType() + ')'); + }); + } else { + field.value('', _('No suitable interfaces found')); + } + + field = this.section.option(form.Value, 'subnet', _('IPv4 Subnet (CIDR)')); + field.placeholder = '10.89.0.0/24'; + field.datatype = 'cidr4'; + field.description = _('IPv4 subnet in CIDR notation'); + // field.rmempty = false; + + field = this.section.option(form.Value, 'gateway', _('IPv4 Gateway')); + field.placeholder = '10.89.0.1'; + field.optional = true; + field.datatype = 'ip4addr'; + field.description = _('IPv4 gateway address'); + + field = this.section.option(form.Value, 'ip_range', _('IP Range (CIDR)')); + field.placeholder = '10.89.0.0/28'; + field.optional = true; + field.datatype = 'cidr4'; + field.description = _('Allocate container IP from this range'); + + field = this.section.option(form.Flag, 'ipv6', _('Enable IPv6')); + field.description = _('Enable IPv6 networking'); + + field = this.section.option(form.Flag, 'internal', _('Internal Network')); + field.description = _('Restrict external access to the network'); + + field = this.section.option(form.Flag, 'setup_openwrt', _('Setup OpenWrt Integration')); + field.description = _( + 'Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt.' + ); + + field = this.section.option(form.Value, 'bridge_name', _('Bridge Interface Name')); + field.placeholder = _('Leave empty to auto-generate'); + field.optional = true; + field.datatype = 'netdevname'; + field.depends({ + 'setup_openwrt': '1', + 'driver': 'bridge' + }); + field.description = _( + 'Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it.' + ); + + field = this.section.option(form.ListValue, 'firewall_zone', _('Firewall Zone')); + field.value('_create_new_', _('Create new zone (will be named: podman_)')); + if (this.existingZones.length > 0) { + this.existingZones.forEach((zoneName) => { + field.value(zoneName, zoneName); + }); + } + field.depends('setup_openwrt', '1'); + field.description = _( + 'Choose firewall zone for this network.' + ); + + field = this.section.option(form.TextValue, 'labels', _('Labels')); + field.placeholder = 'key1=value1\nkey2=value2'; + field.rows = 3; + field.optional = true; + field.description = _('Labels in key=value format, one per line'); + }, + + async handleCreate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + await uci.load(['network', 'firewall', 'dhcp']); + const ulaPrefix = uci.get('network', 'globals', 'ula_prefix'); + + const podnetwork = this.getFieldValues(); + + const setupOpenwrt = podnetwork.setup_openwrt === '1'; + const driver = podnetwork.driver || 'bridge'; + const requestedZone = podnetwork.firewall_zone || '_create_new_'; + const zoneName = requestedZone === '_create_new_' ? 'podman_' + podnetwork.name : + requestedZone; + const baseBridgeName = podnetwork.bridge_name || podnetwork.name; + + let bridgeName = baseBridgeName; + let i = 1; + while (await network.getDevice(bridgeName)) { + bridgeName = `${baseBridgeName}${i}`; + i++; + } + + if ((driver === 'macvlan' || driver === 'ipvlan') && !podnetwork.parent) { + this.error(_('Parent interface is required for macvlan/ipvlan networks')); + return; + } + + // if (setupOpenwrt && !podnetwork.subnet) { + // this.error(_('OpenWrt integration requires subnet to be specified')); + // return; + // } + + if (!podnetwork.gateway && podnetwork.subnet) { + podnetwork.gateway = utils.ipv4.firstHost(podnetwork.subnet); + } + + const payload = { + name: podnetwork.name, + driver: driver + }; + + if (driver === 'bridge') { + payload.network_interface = bridgeName; + } else if (driver === 'macvlan' || driver === 'ipvlan') { + payload.network_interface = podnetwork.parent; + } + + if (podnetwork.subnet) { + payload.subnets = [{ + subnet: podnetwork.subnet + }]; + if (podnetwork.gateway) payload.subnets[0].gateway = podnetwork.gateway; + + if (podnetwork.ip_range) { + const range = utils.ipv4.cidrToRange(podnetwork.ip_range); + if (range) payload.subnets[0].lease_range = range; + } + } + + payload.ipv6_enabled = false; + if (podnetwork.ipv6 === '1') { + const ipv6obj = utils.ipv6.deriveUlaFromIpv4(podnetwork.subnet, ulaPrefix); + payload.ipv6_enabled = true; + + if (podnetwork.subnet) { + payload.subnets.push({ + subnet: ipv6obj.ipv6subnet + }); + + if (podnetwork.gateway) + payload.subnets[1].gateway = ipv6obj.ipv6gateway; + } + } + + payload.internal = podnetwork.internal === '1'; + if (podnetwork.labels) { + payload.labels = {}; + podnetwork.labels.split('\n').forEach((line) => { + const parts = line.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + if (key) payload.labels[key] = value; + } + }); + } + + const createFn = async () => { + const newNetwork = await podmanRPC.networks.create(payload); + + if (!setupOpenwrt) return; + + const inspectData = await Network.getSingleton({ + name: podnetwork.name + }).inspect(); + const NetworkModel = Network.getSingleton(inspectData); + const deviceName = await NetworkModel.integrationCreateDevice(); + await NetworkModel.integrationCreateNetwork(deviceName); + + if (await NetworkModel.hasDnsmasq()) { + await NetworkModel.integrationAddDnsmasqExclusion(); + } + + if (await firewall.getZone(zoneName)) { + const currentNetworks = uci.get('firewall', zoneName, 'network'); + const networkList = Array.isArray(currentNetworks) ? currentNetworks : + currentNetworks ? [currentNetworks] : []; + if (!networkList.includes(podnetwork.name)) { + networkList.push(podnetwork.name); + uci.set('firewall', zoneName, 'network', networkList); + } + } else { + const zoneId = uci.add('firewall', 'zone'); + uci.set('firewall', zoneId, 'name', zoneName); + uci.set('firewall', zoneId, 'input', 'DROP'); + uci.set('firewall', zoneId, 'output', 'ACCEPT'); + uci.set('firewall', zoneId, 'forward', 'REJECT'); + uci.set('firewall', zoneId, 'network', [podnetwork.name]); + } + + const dnsRuleName = 'Allow-' + zoneName + '-DNS'; + const existingDnsRule = uci.sections('firewall', 'rule').find((s) => + uci.get('firewall', s['.name'], 'name') === dnsRuleName + ); + + if (!existingDnsRule) { + const ruleId = uci.add('firewall', 'rule'); + uci.set('firewall', ruleId, 'name', dnsRuleName); + uci.set('firewall', ruleId, 'src', zoneName); + uci.set('firewall', ruleId, 'dest_port', '53'); + uci.set('firewall', ruleId, 'target', 'ACCEPT'); + } + + await uci.save(); + ui.changes.apply(); + await network.flushCache(); + }; + + return this.super('handleCreate', [createFn, _('Network')]); + }, + + isValidParentDevice(device) { + const type = device.getType(); + const name = device.getName(); + return ['ethernet', 'bridge', 'vlan'].includes(type) && + name !== 'lo' && + !name.match(/^(podman|cni-|docker|veth)/); + } +}); + +return baseclass.extend({ + init: PodmanFormNetwork +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/pod.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/pod.js new file mode 100644 index 000000000..a34c62185 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/pod.js @@ -0,0 +1,178 @@ +'use strict'; + +'require baseclass'; +'require form'; + +'require podman.form as podmanForm'; +'require podman.utils as podmanUtil'; +'require podman.rpc as podmanRPC'; +'require podman.view as podmanView'; + +const PodmanFormPod = podmanView.form.extend({ + __name__: 'Podman.Form.Pod', + + makeData() { + return { + pod: { + name: '', + hostname: null, + share: ['cgroup', 'ipc', 'net', 'uts'], + infra: '1', + infra_image: null, + network: 'bridge', + dns_server: null, + dns_search: null, + cpus: null, + cpuset_cpus: null, + memory: null, + labels: null, + } + }; + }, + + async createForm() { + const networks = await podmanRPC.networks.list(); + + let field; + + field = this.section.option(form.Value, 'name', _('Pod Name')); + field.placeholder = 'my-pod'; + field.optional = true; + field.datatype = 'and(uciname,maxlength(63))'; + field.description = _('Leave empty to auto-generate'); + + field = this.section.option(form.Value, 'hostname', _('Hostname')); + field.placeholder = 'pod-host'; + field.optional = true; + field.datatype = 'hostname'; + field.description = _('Hostname for the pod (defaults to pod name)'); + + field = this.section.option(form.MultiValue, 'share', _('Shared Namespaces')); + field.value('cgroup', 'cgroup'); + field.value('ipc', 'ipc'); + field.value('net', 'net'); + field.value('pid', 'pid'); + field.value('uts', 'uts'); + field.description = _('Linux namespaces shared between containers in the pod. Default matches `podman pod create`.'); + + field = this.section.option(form.Flag, 'infra', _('Create Infra Container')); + field.description = _('Required for shared network namespace. Disable only if you know you do not need it.'); + + field = this.section.option(form.Value, 'infra_image', _('Infra Image')); + field.depends('infra', '1'); + field.placeholder = 'k8s.gcr.io/pause:3.5'; + field.optional = true; + field.description = _('Override the image used for the infra container'); + + field = this.section.option(form.ListValue, 'network', _('Network')); + field.depends('infra', '1'); + field.value('bridge', 'bridge (default)'); + field.value('host', 'host'); + field.value('none', 'none'); + networks.forEach((net) => { + const name = net.getName(); + if (name && name !== 'bridge' && name !== 'host' && name !== 'none') { + field.value(name, name); + } + }); + field.description = _('Network namespace for the pod. Containers in the pod share this network.'); + + field = this.section.option(form.DynamicList, 'dns_server', _('DNS Servers')); + field.depends('infra', '1'); + field.datatype = 'ipaddr'; + field.optional = true; + field.description = _('DNS servers for the infra container (shared with all pod containers)'); + + field = this.section.option(form.DynamicList, 'dns_search', _('DNS Search Domains')); + field.depends('infra', '1'); + field.optional = true; + field.description = _('DNS search domains for the infra container'); + + field = this.section.option(form.Value, 'cpus', _('CPU Limit')); + field.placeholder = '1.0'; + field.optional = true; + field.datatype = 'ufloat'; + field.description = _('Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)'); + + field = this.section.option(form.Value, 'cpuset_cpus', _('CPU Set')); + field.placeholder = '0-3'; + field.optional = true; + field.description = _('CPUs the pod is allowed to run on (e.g., "0-3" or "0,2")'); + + field = this.section.option(podmanForm.field.MemoryValue, 'memory', _('Memory Limit')); + field.optional = true; + field.description = _('Memory limit for the pod (e.g., 512m, 1g)'); + + field = this.section.option(form.TextValue, 'labels', _('Labels')); + field.placeholder = 'key1=value1\nkey2=value2'; + field.rows = 3; + field.optional = true; + field.description = _('One per line, format: key=value'); + }, + + async handleCreate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + + const data = this.getFieldValues(); + const spec = {}; + + if (data.name) spec.name = data.name; + if (data.hostname) spec.hostname = data.hostname; + + const shareRaw = data.share; + spec.share = Array.isArray(shareRaw) + ? shareRaw + : (typeof shareRaw === 'string' && shareRaw.trim() ? shareRaw.trim().split(/\s+/) : []); + + const hasInfra = data.infra === '1'; + spec.infra = hasInfra; + if (hasInfra && data.infra_image) spec.infra_image = data.infra_image; + + if (hasInfra) { + if (data.network === 'host') { + spec.netns = { nsmode: 'host' }; + } else if (data.network === 'none') { + spec.netns = { nsmode: 'none' }; + } else if (data.network && data.network !== 'bridge') { + spec.Networks = { [data.network]: {} }; + } + + if (data.dns_server && data.dns_server.length) spec.dns_server = data.dns_server; + if (data.dns_search && data.dns_search.length) spec.dns_search = data.dns_search; + } + + if (data.cpus) spec.cpus = parseFloat(data.cpus); + if (data.cpuset_cpus) spec.cpuset_cpus = data.cpuset_cpus; + + if (data.memory) { + const memBytes = podmanUtil.format.parseMemory(data.memory); + if (memBytes > 0) { + spec.resource_limits = { memory: { limit: memBytes } }; + } + } + + if (data.labels) { + spec.labels = {}; + data.labels.split('\n').forEach((line) => { + const parts = line.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + if (key) spec.labels[key] = value; + } + }); + } + + const createFn = () => podmanRPC.pods.create(spec); + + return this.super('handleCreate', [ createFn, _('Pod') ]); + }, +}); + +return baseclass.extend({ + init: PodmanFormPod +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/resource.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/resource.js new file mode 100644 index 000000000..f297bfb61 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/resource.js @@ -0,0 +1,117 @@ +'use strict'; + +'require baseclass'; +'require form'; + +'require podman.utils as podmanUtil'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; + +/** + * Update podman container resources + */ +const PodmanFormResource = podmanView.form.extend({ + __name__: 'Podman.Form.Resource', + + container: null, + + makeData() { + if (!this.container) { + return { resources: { cpuLimit: '', cpuShares: '', memory: '', memorySwap: '' } }; + } + + const hostConfig = this.container.getHostConfig(); + return { + resources: { + cpuLimit: hostConfig.CpuQuota > 0 ? (hostConfig.CpuQuota / 100000).toFixed(2) : '', + cpuShares: hostConfig.CpuShares || '', + memory: hostConfig.Memory > 0 ? podmanUtil.format.bytes(hostConfig.Memory, 0) : '', + memorySwap: hostConfig.MemorySwap > 0 ? podmanUtil.format.bytes(hostConfig.MemorySwap, 0) : '', + } + }; + }, + + async render(container) { + this.container = container; + + return this.super('render', []); + }, + + createForm() { + let field; + + field = this.section.option(form.Value, 'cpuLimit', _('CPU Limit')); + field.datatype = 'ufloat'; + field.placeholder = '0.5, 1.0, 2.0'; + field.description = _('Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited.'); + + field = this.section.option(form.Value, 'cpuShares', _('CPU Shares Weight')); + field.datatype = 'uinteger'; + field.placeholder = '1024'; + field.validate = (_section_id, value) => { + if (value) { + const n = parseInt(value); + if (n < 0 || n > 262144) return _('Must be between 0 and 262144'); + } + return true; + }; + field.description = _('CPU shares (relative weight). 0 = use default.'); + + field = this.section.option(podmanForm.field.MemoryValue, 'memory', _('Memory Limit')); + field.description = _('Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default.'); + + field = this.section.option(podmanForm.field.MemoryValue, 'memorySwap', _('Memory + Swap Limit')); + field.description = _('Total memory limit (memory + swap). -1 = unlimited, 0 = use default.'); + + field = this.section.option(form.Button, '_update', ' '); + field.inputtitle = _('Update Resources'); + field.inputstyle = 'save'; + field.onclick = () => this.handleUpdate(); + }, + + async handleUpdate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + + const data = this.getFieldValues(); + const updateData = { + cpu: { + quota: 0, + period: 0, + shares: parseInt(data.cpuShares) || 0, + }, + }; + + const parseMemory = (val) => + (!val || val === '-1' || val === '0') ? -1 : podmanUtil.format.parseMemory(val); + + const memory = parseMemory(data.memory); + const memorySwap = parseMemory(data.memorySwap); + + if (data.cpuLimit) { + const period = 100000; + updateData.cpu.quota = Math.floor(parseFloat(data.cpuLimit) * period); + updateData.cpu.period = period; + } + + updateData.memory = { limit: memory }; + if (memory === -1 || memorySwap === -1) { + updateData.memory.swap = -1; + } else if (memorySwap > 0) { + updateData.memory.swap = memorySwap; + } else { + updateData.memory.swap = null; + } + + const createFn = () => this.container.update(updateData); + + return this.super('handleCreate', [ createFn, _('Saving resource'), _('Updating container'), _('Container updated successfully') ]); + }, +}); + +return baseclass.extend({ + init: PodmanFormResource +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/secret.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/secret.js new file mode 100644 index 000000000..9fed6b27d --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/secret.js @@ -0,0 +1,89 @@ +'use strict'; + +'require baseclass'; +'require form'; + +'require podman.rpc as podmanRPC'; +'require podman.view as podmanView'; + +/** + * Create podman secret + */ +const PodmanFormSecret = podmanView.form.extend({ + __name__: 'Podman.Form.Secret', + + makeData() { + return { + secret: { + name: null, + data: null, + labels: null, + } + }; + }, + + createForm() { + let field; + + field = this.section.option(form.Value, 'name', _('Secret Name')); + field.placeholder = 'my-secret'; + field.rmempty = false; + field.datatype = 'maxlength(253)'; + field.validate = (_section_id, value) => { + if (!/^[a-zA-Z0-9_-]+$/.test(value)) { + return _( + 'Secret name can only contain letters, numbers, underscores, and hyphens' + ); + } + return true; + }; + field.description = _( + '1-253 characters: letters, numbers, underscore (_), hyphen (-) only'); + + field = this.section.option(form.TextValue, 'data', _('Secret Data')); + field.placeholder = _('Enter secret data (password, token, key, etc.)'); + field.rmempty = false; + field.rows = 6; + field.description = _('The sensitive data to store securely'); + + field = this.section.option(form.TextValue, 'labels', _('Labels')); + field.placeholder = 'key1=value1\nkey2=value2'; + field.rows = 3; + field.optional = true; + field.description = _('Labels in key=value format, one per line'); + }, + + async handleCreate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + + const data = this.getFieldValues(); + + if (!data.name || !data.data) { + return; + } + + const labels = {}; + if (data.labels) { + data.labels.split('\n').forEach((line) => { + const parts = line.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + if (key) labels[key] = value; + } + }); + } + + const createFn = () => podmanRPC.secrets.create(data.name, data.data, labels); + + return this.super('handleCreate', [ createFn, _('Secret') ]); + }, +}); + +return baseclass.extend({ + init: PodmanFormSecret +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/volume.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/volume.js new file mode 100644 index 000000000..293f60c00 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/volume.js @@ -0,0 +1,97 @@ +'use strict'; + +'require baseclass'; +'require form'; + +'require podman.rpc as podmanRPC'; +'require podman.view as podmanView'; + +/** + * Create podman volume + */ +const PodmanFormVolume = podmanView.form.extend({ + __name__: 'Podman.Form.Volume', + + sectionName: 'volume', + + makeData() { + return { + volume: { + name: null, + driver: 'local', + options: null, + labels: null, + } + }; + }, + + createForm() { + let field; + + field = this.section.option(form.Value, 'name', _('Volume Name')); + field.placeholder = 'my-volume'; + field.optional = true; + field.datatype = 'maxlength(253)'; + field.description = _('Volume name. Leave empty to auto-generate.'); + + field = this.section.option(form.ListValue, 'driver', _('Driver')); + field.value('local', 'local'); + field.value('image', 'image'); + field.description = _('Volume driver to use'); + + field = this.section.option(form.Value, 'options', _('Mount Options')); + field.placeholder = 'type=tmpfs,device=tmpfs,o=size=100m'; + field.optional = true; + field.description = _('Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)'); + + field = this.section.option(form.TextValue, 'labels', _('Labels')); + field.placeholder = 'key1=value1\nkey2=value2'; + field.rows = 3; + field.optional = true; + field.description = _('Labels in key=value format, one per line'); + }, + + async handleCreate() { + if (!this.isValid()) { + return this.scrollToInvalid(); + } + + await this.save(); + + const data = this.getFieldValues(); + const payload = { + Name: data.name, + Driver: data.driver, + }; + + if (data.options) { + payload.Options = {}; + data.options.split(',').forEach((opt) => { + const parts = opt.split('='); + if (parts.length >= 2) { + payload.Options[parts[0].trim()] = parts.slice(1).join('=').trim(); + } + }); + } + + if (data.labels) { + payload.Labels = {}; + data.labels.split('\n').forEach((line) => { + const parts = line.split('='); + if (parts.length >= 2) { + const key = parts[0].trim(); + const value = parts.slice(1).join('=').trim(); + if (key) payload.Labels[key] = value; + } + }); + } + + const createFn = () => podmanRPC.volumes.create(payload); + + return this.super('handleCreate', [ createFn, _('Volume') ]); + }, +}); + +return baseclass.extend({ + init: PodmanFormVolume, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/form/volumeImport.js b/luci-app-podman/htdocs/luci-static/resources/podman/form/volumeImport.js new file mode 100644 index 000000000..e3e794383 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/form/volumeImport.js @@ -0,0 +1,75 @@ +'use strict'; + +'require baseclass'; +'require form'; +'require request'; + +'require podman.rpc as podmanRPC'; +'require podman.view as podmanView'; + +/** + * Import podman volume + */ +const PodmanFormVolumeImport = podmanView.form.extend({ + __name__: 'Podman.Form.VolumeImport', + + sectionName: 'volume', + + isCompressed: false, + file: null, + + makeData() { + return { + volume: { + name: null, + } + }; + }, + + async render(file) { + this.file = file; + + return this.super('render', []); + }, + + createForm() { + const isCompressed = this.file.name.endsWith('.tar.gz') || this.file.name.endsWith('.tgz'); + const volumeName = this.file.name.replace(/\.(tar\.gz|tgz|tar)$/, ''); + + this.isCompressed = isCompressed; + + const field = this.section.option(form.Value, 'name', _('Volume Name')); + field.placeholder = 'my-volume'; + field.rmempty = false; + field.datatype = 'rangelength(1,253)'; + field.default = volumeName; + }, + + async handleCreate() { + await this.save(); + + const name = this.getFieldValue('name'); + + this.loading(_('Uploading file...')); + + const formData = new FormData(); + formData.append('sessionid', L.env.sessionid); + formData.append('filename', '/tmp/podman-import'); + formData.append('filedata', this.file); + + const response = await request.post(L.env.cgi_base + '/cgi-upload', formData); + if (!response.ok) { + this.error(_('Upload failed: %s').format(response.statusText)); + return; + } + + this.loading(_('Importing volume...')); + + await podmanRPC.volumes.import(name, this.isCompressed); + this.success(_('Volume imported successfully')); + }, +}); + +return baseclass.extend({ + init: PodmanFormVolumeImport, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Container.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Container.js new file mode 100644 index 000000000..e697b9951 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Container.js @@ -0,0 +1,785 @@ +'use strict'; + +'require baseclass'; + +'require podman.model.Model as Model'; +'require podman.model.Network as Network'; +'require podman.model.Image as Image'; +'require podman.constants as constants'; +'require podman.utils as podmanUtil'; + + +const InitScript = { + generate: Model.declareRPC({ + object: 'podman', + method: 'init_script_generate', + params: ['name'] + }), + + show: Model.declareRPC({ + object: 'podman', + method: 'init_script_show', + params: ['name'] + }), + + status: Model.declareRPC({ + object: 'podman', + method: 'init_script_status', + params: ['name'] + }), + + setEnabled: Model.declareRPC({ + object: 'podman', + method: 'init_script_set_enabled', + params: ['name', 'enabled'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'init_script_remove', + params: ['name'] + }) +}; + +const ContainerRPC = { + inspect: Model.declareRPC({ + object: 'podman', + method: 'container_inspect', + params: ['id'] + }), + + update: Model.declareRPC({ + object: 'podman', + method: 'container_update', + params: ['id', 'data'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'container_remove', + params: ['id', 'force', 'depend'] + }), + + start: Model.declareRPC({ + object: 'podman', + method: 'container_start', + params: ['id'] + }), + + stop: Model.declareRPC({ + object: 'podman', + method: 'container_stop', + params: ['id'] + }), + + restart: Model.declareRPC({ + object: 'podman', + method: 'container_restart', + params: ['id'] + }), + + pause: Model.declareRPC({ + object: 'podman', + method: 'container_pause', + params: ['id'] + }), + + unpause: Model.declareRPC({ + object: 'podman', + method: 'container_unpause', + params: ['id'] + }), + + rename: Model.declareRPC({ + object: 'podman', + method: 'container_rename', + params: ['id', 'name'] + }), + + create: Model.declareRPC({ + object: 'podman', + method: 'container_create', + params: ['data'] + }), +}; + +const Container = Model.base.extend({ + __name__: 'Podman.Model.Container', + + getID() { + return this.Id || ''; + }, + + getName() { + if (this.Names && Array.isArray(this.Names) && this.Names.length > 0) { + return this.Names[0]; + } + + if (this.Names && !Array.isArray(this.Names) && this.Names !== '') { + return this.Names; + } + + if (this.Name) { + return this.Name; + } + + return _('Unknown'); + }, + + getState() { + if (typeof this.State === 'string') { + return this.State; + } + + return this.State?.Status || this.Status || ''; + }, + + getStateBadge() { + return E('div', { class: `badge ${this.getState()}` }, [ this.getState() ]); + }, + + getAutoUpdateLabel() { + const labels = this.Config?.Labels || this.Labels; + if (!labels) { + return false; + } + return labels['io.containers.autoupdate'] || false; + }, + + getCmdString() { + return this.Config?.Cmd ? this.Config.Cmd.join(' ') : ''; + }, + + getEntrypointString() { + return this.Config?.Entrypoint ? this.Config.Entrypoint.join(' ') : ''; + }, + + getCreateCommandString() { + return this.Config?.CreateCommand ? this.Config?.CreateCommand.join(' ') : ''; + }, + + inspectToSpec(imageRef) { + const config = this.Config || {}; + const hostConfig = this.HostConfig || {}; + const networkSettings = this.NetworkSettings || {}; + const mounts = this.Mounts || []; + + const spec = { image: imageRef }; + + // Name (strip leading slash added by Podman inspect) + if (this.Name) spec.name = this.Name.replace(/^\//, ''); + + // Process + if (config.Cmd && config.Cmd.length) spec.command = config.Cmd; + if (config.Entrypoint && config.Entrypoint.length) spec.entrypoint = config.Entrypoint; + if (config.Tty) spec.terminal = true; + if (config.OpenStdin) spec.stdin = true; + if (config.WorkingDir) spec.work_dir = config.WorkingDir; + if (config.User) spec.user = config.User; + if (config.Hostname) spec.hostname = config.Hostname; + if (config.StopSignal) { + const sigMap = { + SIGHUP: 1, SIGINT: 2, SIGQUIT: 3, SIGILL: 4, SIGTRAP: 5, + SIGABRT: 6, SIGBUS: 7, SIGFPE: 8, SIGKILL: 9, SIGUSR1: 10, + SIGSEGV: 11, SIGUSR2: 12, SIGPIPE: 13, SIGALRM: 14, SIGTERM: 15, + SIGCHLD: 17, SIGCONT: 18, SIGSTOP: 19, SIGTSTP: 20, SIGTTIN: 21, + SIGTTOU: 22, SIGURG: 23, SIGXCPU: 24, SIGXFSZ: 25, SIGVTALRM: 26, + SIGPROF: 27, SIGWINCH: 28, SIGIO: 29, SIGPWR: 30, SIGSYS: 31, + }; + const sigStr = String(config.StopSignal).toUpperCase().replace(/^SIG/, ''); + const sigNum = sigMap['SIG' + sigStr] ?? parseInt(config.StopSignal, 10); + if (sigNum > 0) spec.stop_signal = sigNum; + } + if (config.StopTimeout) spec.stop_timeout = config.StopTimeout; + if (config.Healthcheck) spec.healthconfig = config.Healthcheck; + + // Environment: ["KEY=val", ...] → {KEY: "val", ...} + if (config.Env && config.Env.length) { + spec.env = {}; + for (const e of config.Env) { + const idx = e.indexOf('='); + if (idx > 0) spec.env[e.substring(0, idx)] = e.substring(idx + 1); + } + } + + // Labels + if (config.Labels && Object.keys(config.Labels).length) + spec.labels = config.Labels; + + // HostConfig + if (hostConfig.Privileged) spec.privileged = true; + if (hostConfig.ReadonlyRootfs) spec.read_only_filesystem = true; + if (hostConfig.AutoRemove) spec.remove = true; + if (hostConfig.CapAdd && hostConfig.CapAdd.length) spec.cap_add = hostConfig.CapAdd; + if (hostConfig.CapDrop && hostConfig.CapDrop.length) spec.cap_drop = hostConfig.CapDrop; + if (hostConfig.Dns && hostConfig.Dns.length) spec.dns_server = hostConfig.Dns; + if (hostConfig.DnsOptions && hostConfig.DnsOptions.length) spec.dns_option = hostConfig.DnsOptions; + if (hostConfig.DnsSearch && hostConfig.DnsSearch.length) spec.dns_search = hostConfig.DnsSearch; + if (hostConfig.ExtraHosts && hostConfig.ExtraHosts.length) spec.hostadd = hostConfig.ExtraHosts; + if (hostConfig.GroupAdd && hostConfig.GroupAdd.length) spec.groups = hostConfig.GroupAdd; + if (hostConfig.ShmSize) spec.shm_size = hostConfig.ShmSize; + if (hostConfig.OomScoreAdj) spec.oom_score_adj = hostConfig.OomScoreAdj; + + // Restart policy + const rp = hostConfig.RestartPolicy; + if (rp && rp.Name && rp.Name !== 'no') { + spec.restart_policy = rp.Name; + if (rp.MaximumRetryCount) spec.restart_tries = rp.MaximumRetryCount; + } + + // Exposed ports: {"8080/tcp": {}} → {8080: "tcp"} + const exposedPorts = config.ExposedPorts || {}; + if (Object.keys(exposedPorts).length) { + spec.expose = {}; + for (const portProto of Object.keys(exposedPorts)) { + const [port, proto] = portProto.split('/'); + const portNum = parseInt(port, 10); + if (portNum > 0 && portNum <= 65535) + spec.expose[portNum] = proto || 'tcp'; + } + } + + // Port mappings: {"8080/tcp": [{HostIp, HostPort}]} → array + const portBindings = hostConfig.PortBindings || {}; + if (Object.keys(portBindings).length) { + spec.portmappings = []; + for (const [portProto, bindings] of Object.entries(portBindings)) { + if (!bindings || !bindings.length) continue; + const [port, protocol] = portProto.split('/'); + const containerPort = parseInt(port, 10); + for (const b of bindings) { + const m = { container_port: containerPort, protocol: protocol || 'tcp' }; + if (b.HostPort) m.host_port = parseInt(b.HostPort, 10); + if (b.HostIp && b.HostIp !== '0.0.0.0') m.host_ip = b.HostIp; + spec.portmappings.push(m); + } + } + } + + // Resource limits - use CFS quota/period directly (most precise) + if (hostConfig.Memory > 0 || hostConfig.CpuQuota > 0) { + spec.resource_limits = {}; + if (hostConfig.Memory > 0) + spec.resource_limits.memory = { limit: hostConfig.Memory }; + if (hostConfig.CpuQuota > 0) { + spec.resource_limits.cpu = { quota: hostConfig.CpuQuota }; + if (hostConfig.CpuPeriod > 0) + spec.resource_limits.cpu.period = hostConfig.CpuPeriod; + } + } + + // Volumes and bind mounts + const namedVols = mounts.filter(m => m.Type === 'volume'); + const bindMounts = mounts.filter(m => m.Type === 'bind'); + + if (namedVols.length) { + spec.volumes = namedVols.map(m => ({ + Name: m.Name, + Dest: m.Destination, + Options: m.Options || [] + })); + } + + if (bindMounts.length) { + spec.mounts = bindMounts.map(m => ({ + Source: m.Source, + Destination: m.Destination, + ReadOnly: !m.RW, + options: m.Options || [] + })); + } + + // Network + const nm = hostConfig.NetworkMode || ''; + if (nm === 'host') { + spec.netns = { nsmode: 'host' }; + } else if (nm === 'none') { + spec.netns = { nsmode: 'none' }; + } else { + const nets = networkSettings.Networks || {}; + const nonDefault = Object.keys(nets).filter(n => n !== 'bridge' && n !== 'default'); + if (nonDefault.length) { + spec.networks = {}; + for (const n of nonDefault) spec.networks[n] = {}; + } + } + + return spec; + }, + + getWorkingDir() { + return this.Config?.WorkingDir; + }, + + getHostname() { + return this.Config?.Hostname; + }, + + getUser() { + return this.Config?.User; + }, + + getInteractive() { + return this.Config?.OpenStdin; + }, + + getPrivileged() { + return this.HostConfig?.Privileged; + }, + + getNetworkMode() { + return this.HostConfig?.NetworkMode; + }, + + getNetworkSettings() { + return this.NetworkSettings || {}; + }, + + getConfig() { + return this.Config || {}; + }, + + getHostConfig() { + return this.HostConfig || {}; + }, + + getTty() { + return this.Config?.Tty; + }, + + getMounts() { + return this.Mounts || []; + }, + + getEnvironmentVars() { + return this.Config?.Env || []; + }, + + getPorts() { + const ports = []; + const networkPorts = this.getNetworkSettings().Ports || {}; + + Object.keys(networkPorts).forEach((port) => { + const [portOnly, protocol] = port.split('/'); + const portBindings = networkPorts[port] || []; + + if (portBindings.length === 0) { + ports.push({ string: port, port: portOnly, protocol: protocol || '', hostPort: null, hostIp: null }); + return; + } + + portBindings.forEach((portBinding) => { + const bindingString = [port]; + if (portBinding.HostIp) bindingString.push(portBinding.HostIp); + if (portBinding.HostPort) bindingString.push(portBinding.HostPort); + ports.push({ + string: bindingString.join(':'), + port: portOnly, + protocol: protocol || '', + hostIp: portBinding.HostIp, + hostPort: portBinding.HostPort, + }); + }); + }); + + return ports; + }, + + getImageName() { + return (this.ImageName || this.Image).replace(/^[^\/]+\//, '').split(':')[0]; + }, + + _parseImageRef() { + const cleanUrl = this.Image.split('@')[0]; + const lastColon = cleanUrl.lastIndexOf(':'); + const lastSlash = cleanUrl.lastIndexOf('/'); + return { cleanUrl, lastColon, lastSlash }; + }, + + getImageRepo() { + const { cleanUrl, lastColon, lastSlash } = this._parseImageRef(); + return lastColon > lastSlash ? cleanUrl.substring(0, lastColon) : ''; + }, + + getImageTag() { + const { cleanUrl, lastColon, lastSlash } = this._parseImageRef(); + return lastColon > lastSlash ? cleanUrl.substring(lastColon + 1) : 'latest'; + }, + + getCreated(dateFormat) { + return dateFormat ? podmanUtil.format.date(this.Created) : this.Created; + }, + + getStartedAt(dateFormat) { + const date = this.StartedAt || this.State?.StartedAt; + return dateFormat ? podmanUtil.format.date(date) : date; + }, + + getRestartPolicyName() { + if (this.HostConfig?.RestartPolicy?.Name) { + return this.HostConfig.RestartPolicy.Name; + } + + return 'no'; + }, + + getDetailLink(text) { + return E('a', { + class: 'text-bold', + href: this.getDetailUrl(), + 'data-container-id': this.getID() + }, text || this.getID()); + }, + + getDetailUrl() { + return L.url('admin/podman/container', this.getID()); + }, + + getAutoStartStatusIcon(status) { + let text = '-'; + + if (status === 'enabled') { + text = constants.ICON.INIT_ENABLED; + } else if (status === 'missing') { + text = constants.ICON.INIT_MISSING; + } else if (status === 'disabled') { + text = constants.ICON.INIT_DISABLED; + } + + return E('span', { class: `autostart-status autostart-${status || ''}` }, text); + }, + + isRunning() { + const state = this.getState(); + return this.State?.Running + || state === 'running' + || state === 'starting' + || state === 'unhealthy'; + }, + + isStopped() { + const state = this.getState(); + return state === 'exited' || state === 'created'; + }, + + isPaused() { + return !!this.State?.Paused || this.getState() === 'paused'; + }, + + async checkInitScript() { + if (!this.HostConfig) { + const inspectData = await this.inspect(); + if (!inspectData) return 'none'; + this.HostConfig = inspectData.HostConfig; + } + const hasRestartPolicy = this._hasRestartPolicy(); + + if (!hasRestartPolicy) { + return 'none'; + } + + const initStatus = await InitScript.status(this.getName()); + if (!initStatus) return 'none'; + + if (initStatus.exists && initStatus.enabled) { + return 'enabled'; + } else if (!initStatus.exists) { + return 'missing'; + } else if (initStatus.exists && !initStatus.enabled) { + return 'disabled'; + } + + return 'none'; + }, + + async generateInitScript() { + return InitScript.generate(this.getName()); + }, + + async showInitScript() { + return InitScript.show(this.getName()); + }, + + async removeInitScript() { + return InitScript.remove(this.getName()); + }, + + async enableInitScript() { + return InitScript.setEnabled(this.getName(), true); + }, + + async disableInitScript() { + return InitScript.setEnabled(this.getName(), false); + }, + + _hasRestartPolicy() { + return this.getRestartPolicyName() !== 'no'; + }, + + async inspect() { + return ContainerRPC.inspect(this.getID()); + }, + + async update(data) { + await ContainerRPC.update(this.getID(), data); + + const initScriptStatus = await this.checkInitScript(); + const policy = data.RestartPolicy; + const hasRestartPolicy = policy !== 'no'; + + if (hasRestartPolicy && (initScriptStatus === 'none' || initScriptStatus === 'missing')) { + await this.generateInitScript(); + await this.enableInitScript(); + } else if (!hasRestartPolicy && (initScriptStatus === 'enabled' || initScriptStatus === 'disabled')) { + await this.removeInitScript(); + } + }, + + async remove(force, volumes) { + const initScriptStatus = await this.checkInitScript(); + if (initScriptStatus === 'enabled' || initScriptStatus === 'disabled') { + await this.removeInitScript(); + } + return ContainerRPC.remove(this.getID(), force ?? false, volumes ?? false); + }, + + async start() { + return ContainerRPC.start(this.getID()); + }, + + async stop() { + return ContainerRPC.stop(this.getID()); + }, + + async restart() { + return ContainerRPC.restart(this.getID()); + }, + + async pause() { + return ContainerRPC.pause(this.getID()); + }, + + async unpause() { + return ContainerRPC.unpause(this.getID()); + }, + + async connect(networkName, payload) { + return Network.getSingleton({ name: networkName }).connect(Object.assign({ container: this.getID() }, payload)); + }, + + async disconnect(networkName) { + return Network.getSingleton({ name: networkName }).disconnect({ Container: this.getID() }); + }, + + streamTop(onChunk, delay, psargs) { + const params = new URLSearchParams({ delay: String(delay || 2) }); + if (psargs) params.set('ps_args', psargs); + const url = L.url('admin/podman/stream/top', this.getID()) + '?' + params; + return this._stream( + () => url, + (data) => { if (!data.raw && !data.Error) onChunk(data); } + ); + }, + + streamStats(onChunk, interval) { + const params = new URLSearchParams({ interval: String(interval || 2) }); + const url = L.url('admin/podman/stream/stats', this.getID()) + '?' + params; + return this._stream( + () => url, + (data) => { + if (data.raw || data.Error || !data.Stats || data.Stats.length === 0) { + return; + } + onChunk(data.Stats[0]); + } + ); + }, + + streamLogs(onChunk, { tail = 100, since = null, until = null, follow = true } = {}) { + if (!follow) { + // Historical fetch: single request, stop after completion (no reconnect) + let handle; + handle = this._stream( + () => { + const params = new URLSearchParams(); + params.set('tail', String(tail)); + params.set('follow', 'false'); + if (since !== null) params.set('since', String(since)); + if (until !== null) params.set('until', String(until)); + return L.url('admin/podman/stream/logs', this.getID()) + '?' + params; + }, + onChunk, + () => handle?.stop() + ); + return handle; + } + + // Live stream: reconnect with since= after each disconnect + let reconnectSince = null; + return this._stream( + () => { + const params = new URLSearchParams(); + if (reconnectSince !== null) { + // Reconnect: continue from last close, no tail (would cap new lines) + params.set('since', String(reconnectSince)); + } else { + // First connect: apply user params + params.set('tail', String(tail)); + if (since !== null) params.set('since', String(since)); + } + params.set('follow', 'true'); + if (until !== null) params.set('until', String(until)); + return L.url('admin/podman/stream/logs', this.getID()) + '?' + params; + }, + onChunk, + () => { reconnectSince = Date.now() / 1000; } + ); + }, + + async checkImageUpdate() { + const findArchDigest = (manifest, arch, os) => { + if (!manifest || !manifest.manifests) { + return manifest.digest || null; + } + + const entry = manifest.manifests.find((m) => + m.platform && + m.platform.architecture === arch && + m.platform.os === os + ); + + return entry ? entry.digest : null; + }; + + try { + const image = await this.getImage(); + + const imageRef = image.getDisplayTag() || this.Image; + if (!imageRef || imageRef.startsWith('')) + return { hasUpdate: false }; + + const localArch = image.Architecture || 'amd64'; + const localOs = image.Os || 'linux'; + + const manifest = await image.inspectManifest(imageRef); + + const remoteDigest = findArchDigest(manifest, localArch, localOs); + + const repoDigests = image.RepoDigests || []; + const hasUpdate = !!(remoteDigest && !repoDigests.some(rd => rd.includes(remoteDigest))); + + return { + hasUpdate, + remoteDigest, + }; + } catch (err) { + return { + hasUpdate: false, + error: err.message || String(err) + }; + } + }, + + async _reinstateInitScript(status) { + if (status !== 'enabled' && status !== 'disabled') return; + await this.generateInitScript(); + if (status === 'enabled') await this.enableInitScript(); + }, + + getUpdateSkipReason() { + if (this.Pod) + return _('Part of a pod ("%s")').format(this.Pod); + const hostConfig = this.getHostConfig(); + if (hostConfig.AutoRemove) + return _('Ephemeral (--rm flag)'); + if (hostConfig.NetworkMode?.startsWith('container:')) + return _('Shares network with another container'); + return null; + }, + + async updateImage(onProgress) { + const skipReason = this.getUpdateSkipReason(); + if (skipReason) throw new Error(skipReason); + + const [image, initStatus] = await Promise.all([this.getImage(), this.checkInitScript()]); + + if (!image.RepoTags?.length || image.RepoTags[0] === ':') { + const ref = this.ImageName || this.Config?.Image; + if (ref) image.RepoTags = [ref]; + } + + const imageRef = image.getDisplayTag(); + if (!imageRef || imageRef === ':') + throw new Error(_('Container image has no tag - cannot update')); + + onProgress?.(_('Pulling %s').format(imageRef) + '\n'); + const oldImageId = image.getID(); + const newImageId = await image.update(onProgress); + + if (!newImageId) + throw new Error(_('Pull did not return an image ID - pull may have failed')); + + if (newImageId === oldImageId) + throw new Error(_('Image is already up-to-date')); + + const wasRunning = this.isRunning(); + const spec = this.inspectToSpec(newImageId); + + if (wasRunning) { + onProgress?.('→ ' + _('Stopping container') + '\n'); + try { await this.stop(); } + catch (e) { throw new Error(_('Stop failed: %s').format(e.message || e)); } + } + + onProgress?.('→ ' + _('Removing container') + '\n'); + try { await this.remove(); } + catch (e) { throw new Error(_('Remove failed: %s').format(e.message || e)); } + + onProgress?.('→ ' + _('Creating container') + '\n'); + let created; + try { created = await ContainerRPC.create(spec); } + catch (e) { throw new Error(_('Create failed: %s').format(e.message || e)); } + if (!created || created.error) + throw new Error(_('Create failed: %s').format(created?.error || _('Failed to create container'))); + + if (wasRunning) { + onProgress?.('→ ' + _('Starting container') + '\n'); + try { await ContainerRPC.start(created.Id); } + catch (e) { throw new Error(_('Start failed: %s').format(e.message || e)); } + } + + onProgress?.('→ ' + _('Reinstating init script') + '\n'); + try { await this._reinstateInitScript(initStatus); } + catch (e) { throw new Error(_('Init script reinstate failed: %s').format(e.message || e)); } + + onProgress?.('✓ ' + _('Updated to %s').format((newImageId || '').substring(0, 12)) + '\n'); + return { oldImage: image }; + }, + + async getImage() { + const image = Image.getSingleton({ Id: this.ImageID || this.Image }); + return Image.getSingleton(await image.inspect()); + }, + + async updateName(name) { + if (!name || name === this.getName()) return; + + await ContainerRPC.rename(this.getID(), name); + + const initScriptStatus = await this.checkInitScript(); + if (initScriptStatus === 'enabled' || initScriptStatus === 'disabled') { + await this.removeInitScript(); + } + + if (this.Names && this.Names.length > 0) this.Names[0] = name; + if (this.Name) this.Name = name; + + await this._reinstateInitScript(initScriptStatus); + }, +}); + +return baseclass.extend({ + getSingleton(container) { + return Container.extend(container).instantiate([]); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Image.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Image.js new file mode 100644 index 000000000..53330dcb3 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Image.js @@ -0,0 +1,103 @@ +'use strict'; + +'require baseclass'; + +'require podman.model.Model as Model'; + +const ImageRPC = { + inspect: Model.declareRPC({ + object: 'podman', + method: 'image_inspect', + params: ['id'], + }), + + inspectManifest: Model.declareRPCSilent({ + object: 'podman', + method: 'image_manifest_inspect', + params: ['image'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'image_remove', + params: ['id', 'force'], + }), +}; + +const Image = Model.base.extend({ + __name__: 'Podman.Model.Image', + + getID() { + return this.Id; + }, + + getDisplayTag() { + return this.getRepoTags()[0]; + }, + + getRepoTags() { + return this.RepoTags || [':']; + }, + + getRepository() { + const tag = this.getDisplayTag() || ':'; + return tag.split(':')[0] || ''; + }, + + getTag() { + const tag = this.getDisplayTag() || ':'; + return tag.split(':')[1] || ''; + }, + + getDigest() { + if (this.Digest) { + return this.Digest; + } + + if (this.RepoDigests && this.RepoDigests.length > 0) { + for (const rd of this.RepoDigests) { + if (rd.includes('@sha256:')) { + return rd.split('@')[1]; + } + } + } + return null; + }, + + inspect() { + return ImageRPC.inspect(this.getID()); + }, + + inspectManifest(ref) { + return ImageRPC.inspectManifest(ref || this.getDisplayTag()); + }, + + remove() { + return ImageRPC.remove(this.getID()); + }, + + update(onProgress) { + return this.streamPull(onProgress); + }, + + streamPull(onProgress) { + return new Promise((resolve, reject) => { + let handle; + handle = this._stream( + () => L.url('admin/podman/stream/pull') + '?reference=' + encodeURIComponent(this.getDisplayTag()), + (chunk) => { + if (chunk.images && chunk.images.length > 0) { resolve(chunk.images[0]); handle.stop(); return; } + if (chunk.error) { reject(new Error(chunk.error)); handle.stop(); return; } + if (chunk.stream) onProgress?.(chunk.stream); + if (chunk.raw) onProgress?.(chunk.raw + '\n'); + } + ); + }); + }, +}); + +return baseclass.extend({ + getSingleton(image) { + return Image.extend(image).instantiate([]); + } +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Model.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Model.js new file mode 100644 index 000000000..fdb125877 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Model.js @@ -0,0 +1,112 @@ +'use strict'; + +'require baseclass'; +'require rpc'; +'require ui'; + +'require podman.ui as podmanUI'; + +function checkResponse(response) { + if (response?.response >= 400) { + throw new Error(response.message); + } + return response; +} + +function catchError(err) { + const msg = err?.message || String(err); + if (msg.includes('Access denied') || msg.includes('session is expired')) { + window.location.href = L.url('admin'); + return; + } + ui.hideModal(); + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + podmanUI.alert(msg, 'error'); + throw err; +} + +function processLines(buffer, onChunk) { + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; + for (const line of lines) { + if (line.trim()) { + try { + onChunk(JSON.parse(line)); + } catch (e) { + onChunk({ raw: line.trimEnd() }); + } + } + } + return buffer; +} + +const Model = baseclass.extend({ + __name: 'Podman.Model', + + _stream(getUrl, onData, onDone) { + let stopped = false; + let currentController = null; + + const doStream = async () => { + const decoder = new TextDecoder(); + let backoff = 1000; + + while (!stopped) { + currentController = new AbortController(); + try { + const response = await fetch(getUrl(), { signal: currentController.signal }); + if (response.status === 403) { + window.location.reload(); + return; + } + const reader = response.body?.getReader(); + let buffer = ''; + + while (!stopped) { + const { done, value } = await reader.read(); + if (done) { + // Flush any trailing line the server sent without a final '\n' + if (buffer.trim() && !stopped) + processLines(buffer + '\n', onData); + break; + } + buffer += decoder.decode(value, { stream: true }); + buffer = processLines(buffer, onData); + } + + backoff = 1000; + if (!stopped) onDone?.(); + } catch (err) { + if (stopped) break; // AbortController fired from stop() + // network error - reconnect with backoff + await new Promise(r => setTimeout(r, backoff)); + backoff = Math.min(backoff * 2, 30000); + } + } + }; + + doStream(); + + return { + stop: () => { + stopped = true; + if (currentController) currentController.abort(); + } + }; + }, +}); + +return baseclass.extend({ + base: Model, + declareRPC(options) { + const fn = rpc.declare(options); + return (...args) => fn(...args).then(checkResponse).catch(catchError); + }, + // Like declareRPC but does not show the global error dialog on failure. + // Use for background checks where the caller handles the error itself. + declareRPCSilent(options) { + const fn = rpc.declare(options); + return (...args) => fn(...args).then(checkResponse); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Network.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Network.js new file mode 100644 index 000000000..787262c7f --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Network.js @@ -0,0 +1,316 @@ +'use strict'; + +'require baseclass'; +'require network'; +'require firewall'; +'require uci'; +'require ui'; + +'require podman.model.Model as Model'; + +const NetworkRPC = { + inspect: Model.declareRPC({ + object: 'podman', + method: 'network_inspect', + params: ['name'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'network_remove', + params: ['name', 'force'] + }), + + connect: Model.declareRPC({ + object: 'podman', + method: 'network_connect', + params: ['name', 'data'] + }), + + disconnect: Model.declareRPC({ + object: 'podman', + method: 'network_disconnect', + params: ['name', 'data'] + }), +}; + +const Network = Model.base.extend({ + __name__: 'Podman.Model.Network', + + getID() { + return this.ID; + }, + + getName() { + return this.name || _('Unknown'); + }, + + getDriver() { + return this.driver; + }, + + getSubnetIP4() { + if (this.subnets?.length > 0) { + return this.subnets[0].subnet || ''; + } + if (this.IPAM?.Config?.length > 0) { + return this.IPAM.Config[0].Subnet || ''; + } + return ''; + }, + + getSubnetIP6() { + if (this.ipv6_enabled && this.subnets?.length > 1) { + return this.subnets[1].subnet || ''; + } + return ''; + }, + + getGatewayIP4() { + if (this.subnets?.length > 0) { + return this.subnets[0].gateway || ''; + } + if (this.IPAM?.Config?.length > 0) { + return this.IPAM.Config[0].Gateway || ''; + } + return ''; + }, + + getGatewayIP6() { + if (this.ipv6_enabled && this.subnets?.length > 1) { + return this.subnets[1].gateway || ''; + } + return ''; + }, + + isBridge() { + return this.getDriver() === 'bridge'; + }, + + async inspect() { + return NetworkRPC.inspect(this.getName()); + }, + + async remove(force) { + return NetworkRPC.remove(this.getName(), force ?? false); + }, + + async connect(data) { + return NetworkRPC.connect(this.getName(), data || {}); + }, + + async disconnect(data) { + return NetworkRPC.disconnect(this.getName(), data || {}); + }, + + async integrationCheck() { + const check = { + hasInterface: false, + hasDevice: false, + hasDnsmasqExclusion: false, + }; + + const networkObj = await network.getNetwork(this.getName()); + let deviceObj; + + if (networkObj) { + check.hasInterface = true; + deviceObj = networkObj.getDevice(); + + if (deviceObj) { + check.hasDevice = true; + } + } + + if (!deviceObj) { + const devices = (await network.getDevices()).map((device) => device.device); + + if (devices.includes(this.network_interface)) { + check.hasDevice = true; + } + } + + await uci.load('dhcp'); + const mainSection = await this.hasDnsmasq() ? this._getDnsmasqSection() : null; + if (mainSection) { + const notInterfaces = uci.get('dhcp', mainSection, 'notinterface'); + + if ([].concat(notInterfaces ?? []).includes(this.getName())) { + check.hasDnsmasqExclusion = true; + } + } else { + check.hasDnsmasqExclusion = true; + } + + return check; + }, + + async integrationRepair() { + await uci.load('dhcp'); + const status = await this.integrationCheck(); + if (status.hasInterface && status.hasDevice && status.hasDnsmasqExclusion) { + return; + } + + let deviceName; + let changes = false; + + if (status.hasDevice === false && this.isBridge()) { + deviceName = await this.integrationCreateDevice(); + changes = true; + + if (status.hasInterface === true) { + const net = await network.getNetwork(this.getName()); + net.addDevice(deviceName); + } + } + + if (status.hasInterface === false) { + if (status.hasDevice === true) { + deviceName = this.network_interface; + } + + await this.integrationCreateNetwork(deviceName); + changes = true; + } + + if (await this.hasDnsmasq() && status.hasDnsmasqExclusion === false) { + await this.integrationAddDnsmasqExclusion(); + changes = true; + } + + if (changes === false) { + return; + } + + await uci.save(); + ui.changes.apply(); + await network.flushCache(); + }, + + async integrationRemove() { + await uci.load('dhcp'); + const status = await this.integrationCheck(); + let changes = false; + + // Check device shared status BEFORE deleting the interface to avoid stale network cache + let removeDevice = false; + if (status.hasDevice === true && this.isBridge()) { + const device = await network.getDevice(this.network_interface); + removeDevice = device?.getNetworks().length === 1; + } + + if (status.hasInterface === true) { + const zone = await firewall.getZoneByNetwork(this.getName()); + if (zone) await zone.deleteNetwork(this.getName()); + await network.deleteNetwork(this.getName()); + changes = true; + } + + if (removeDevice) { + uci.remove('network', this.network_interface + '_device'); + changes = true; + } + + if (await this.hasDnsmasq() && status.hasDnsmasqExclusion === true) { + const mainSection = this._getDnsmasqSection(); + if (mainSection) { + const notInterfaces = [].concat(uci.get('dhcp', mainSection, 'notinterface') || []) + .filter(iface => iface !== this.getName()); + if (notInterfaces.length > 0) + uci.set('dhcp', mainSection, 'notinterface', notInterfaces); + else + uci.unset('dhcp', mainSection, 'notinterface'); + changes = true; + } + } + + if (changes === false) { + return; + } + + await uci.save(); + ui.changes.apply(); + await network.flushCache(); + }, + + async integrationCreateNetwork(deviceName) { + const networkName = this.getName(); + + if (this.subnets?.length === 0 && this.ipam_options?.driver === 'dhcp') { + uci.add('network', 'interface', networkName); + uci.set('network', networkName, 'proto', 'dhcp'); + return; + } + + const subnet = this.getSubnetIP4(); + if (!subnet) return; + + const prefix = parseInt(subnet.split('/')[1], 10); + const netmask = network.prefixToMask(prefix); + + const sid = `${networkName}`; + uci.add('network', 'interface', sid); + uci.set('network', sid, 'proto', 'static'); + if (deviceName) uci.set('network', sid, 'device', deviceName); + uci.set('network', sid, 'ipaddr', this.getGatewayIP4()); + uci.set('network', sid, 'netmask', netmask); + + if (this.ipv6_enabled) { + uci.set('network', sid, 'ip6addr', this.getGatewayIP6() + '/64'); + } + }, + + async integrationCreateDevice() { + const devices = (await network.getDevices()).map((device) => device.device); + + const baseName = this.network_interface || this.getName(); + let i = 0; + let deviceName = baseName; + + while (devices.includes(deviceName)) { + i++; + deviceName = `${baseName}${i}`; + } + + const sid = `${deviceName}_device`; + uci.add('network', 'device', sid); + uci.set('network', sid, 'type', 'bridge'); + uci.set('network', sid, 'name', deviceName); + uci.set('network', sid, 'bridge_empty', '1'); + uci.set('network', sid, 'ipv6', this.ipv6_enabled ? '1' : '0'); + + if (this.ipv6_enabled) { + uci.set('network', sid, 'ip6assign', '64'); + } + + return deviceName; + }, + + _getDnsmasqSection() { + const sections = uci.sections('dhcp', 'dnsmasq'); + return sections.length > 0 ? sections[0]['.name'] : null; + }, + + async hasDnsmasq() { + return L.hasSystemFeature('dnsmasq'); + }, + + async integrationAddDnsmasqExclusion() { + const mainSection = this._getDnsmasqSection(); + if (mainSection) { + const notInterfaces = [].concat(uci.get('dhcp', mainSection, 'notinterface') || []); + if (!notInterfaces.includes(this.getName())) { + notInterfaces.push(this.getName()); + uci.set('dhcp', mainSection, 'notinterface', notInterfaces); + } + } + }, +}); + +return baseclass.extend({ + __name__: 'Podman.Model.Network', + getSingleton(data) { + return Network.extend(data).instantiate([]); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Pod.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Pod.js new file mode 100644 index 000000000..a359db95c --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Pod.js @@ -0,0 +1,182 @@ +'use strict'; + +'require baseclass'; +'require ui'; + +'require podman.model.Model as Model'; +'require podman.ui as podmanUI'; + +const PodRPC = { + inspect: Model.declareRPC({ + object: 'podman', + method: 'pod_inspect', + params: ['name'] + }), + + start: Model.declareRPC({ + object: 'podman', + method: 'pod_start', + params: ['name'] + }), + + stop: Model.declareRPC({ + object: 'podman', + method: 'pod_stop', + params: ['name'] + }), + + kill: Model.declareRPCSilent({ + object: 'podman', + method: 'pod_kill', + params: ['name', 'signal'] + }), + + restart: Model.declareRPC({ + object: 'podman', + method: 'pod_restart', + params: ['name'] + }), + + pause: Model.declareRPC({ + object: 'podman', + method: 'pod_pause', + params: ['name'] + }), + + unpause: Model.declareRPC({ + object: 'podman', + method: 'pod_unpause', + params: ['name'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'pod_remove', + params: ['name', 'force'] + }), +}; + +const Pod = Model.base.extend({ + __name__: 'Podman.Model.Pod', + + getID() { + return this.Id; + }, + + getName() { + return this.Name || this.Id || _('Unknown'); + }, + + getStatus() { + return this.Status || this.State || ''; + }, + + getStatusBadge() { + const status = this.getStatus(); + const cssClass = status.toLowerCase(); + return E('div', { class: `badge ${cssClass}` }, [ status ]); + }, + + getCreatedAt() { + return this.Created; + }, + + getInfraId() { + return this.InfraId || this.InfraContainerID; + }, + + getNetworks() { + return this.Networks || []; + }, + + getLabels() { + return this.Labels || {}; + }, + + getContainers() { + return this.Containers || []; + }, + + isRunning() { + return this.getStatus() === 'Running'; + }, + + isPaused() { + return this.getStatus() === 'Paused'; + }, + + isStopped() { + const state = this.getStatus(); + return state === 'Stopped' || state === 'Exited' || state === 'Created' || state === 'Dead'; + }, + + async inspect() { + return PodRPC.inspect(this.getName()); + }, + + async start() { + return PodRPC.start(this.getName()); + }, + + async stop() { + const result = await PodRPC.stop(this.getName()); + if (result?.Id) return result; + + try { + return await PodRPC.kill(this.getName(), 'SIGKILL'); + } catch (err) { + const msg = String(err?.message || err); + if (msg.includes('no running containers')) return result; + ui.hideModal(); + podmanUI.alert(msg, 'error'); + throw err; + } + }, + + async restart() { + return PodRPC.restart(this.getName()); + }, + + async pause() { + return PodRPC.pause(this.getName()); + }, + + async unpause() { + return PodRPC.unpause(this.getName()); + }, + + async remove(force) { + return PodRPC.remove(this.getName(), force ?? true); + }, + + streamTop(onChunk, delay) { + const params = new URLSearchParams({ delay: String(delay || 2) }); + const url = L.url('admin/podman/stream/pod_top', this.getName()) + '?' + params; + return this._stream( + () => url, + (data) => { if (!data.raw && !data.Error) onChunk(data); } + ); + }, + + streamStats(onChunk) { + const url = L.url('admin/podman/stream/pod_stats', this.getName()); + return this._stream( + () => url, + (data) => { + if (!Array.isArray(data)) return; + onChunk(data); + } + ); + }, + + getDetailLink(label) { + return E('a', { href: L.url('admin/podman/pod', this.getID()) }, label || this.getName()); + }, +}); + +return baseclass.extend({ + __name__: 'Podman.Model.Pod', + getSingleton(data) { + return Pod.extend(data).instantiate([]); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Secret.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Secret.js new file mode 100644 index 000000000..55b8669a0 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Secret.js @@ -0,0 +1,53 @@ +'use strict'; + +'require baseclass'; + +'require podman.model.Model as Model'; + +const SecretRPC = { + inspect: Model.declareRPC({ + object: 'podman', + method: 'secret_inspect', + params: ['name'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'secret_remove', + params: ['name'] + }), +}; + +const Secret = Model.base.extend({ + __name__: 'Podman.Model.Secret', + + getID() { + return this.ID; + }, + + getName() { + return this.Spec?.Name; + }, + + getDriver() { + return this.Spec?.Driver?.Name; + }, + + getLabels() { + return this.Spec?.Labels ?? {}; + }, + + async inspect() { + return SecretRPC.inspect(this.getName()); + }, + + async remove() { + return SecretRPC.remove(this.getName()); + }, +}); + +return baseclass.extend({ + getSingleton(secret) { + return Secret.extend(secret).instantiate([]); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/model/Volume.js b/luci-app-podman/htdocs/luci-static/resources/podman/model/Volume.js new file mode 100644 index 000000000..853e2294e --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/model/Volume.js @@ -0,0 +1,41 @@ +'use strict'; + +'require baseclass'; + +'require podman.model.Model as Model'; + +const VolumeRPC = { + inspect: Model.declareRPC({ + object: 'podman', + method: 'volume_inspect', + params: ['name'] + }), + + remove: Model.declareRPC({ + object: 'podman', + method: 'volume_remove', + params: ['name', 'force'] + }), +}; + +const Volume = Model.base.extend({ + __name__: 'Podman.Model.Volume', + + getName() { + return this.Name || _('Unknown'); + }, + + async inspect() { + return VolumeRPC.inspect(this.getName()); + }, + + async remove(force) { + return VolumeRPC.remove(this.getName(), force || false); + }, +}); + +return baseclass.extend({ + getSingleton(volume) { + return Volume.extend(volume).instantiate([]); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/rpc.js b/luci-app-podman/htdocs/luci-static/resources/podman/rpc.js new file mode 100644 index 000000000..9c9e873d3 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/rpc.js @@ -0,0 +1,187 @@ +'use strict'; + +'require baseclass'; +'require podman.model.Model as Model'; +'require podman.model.Container as Container'; +'require podman.model.Image as Image'; +'require podman.model.Network as Network'; +'require podman.model.Volume as Volume'; +'require podman.model.Secret as Secret'; +'require podman.model.Pod as Pod'; + +return baseclass.extend({ + /** + * Container methods. + */ + containers: { + list: Model.declareRPC({ + object: 'podman', + method: 'containers_list', + params: ['query'], + expect: { + data: [] + }, + filter: (containers) => containers + .map((container) => Container.getSingleton(container)) + .sort((containerA, containerB) => containerA + .getName() + .localeCompare(containerB.getName())) + }), + + create: Model.declareRPC({ + object: 'podman', + method: 'container_create', + params: ['data'] + }), + }, + + /** + * Image methods. + */ + images: { + list: Model.declareRPC({ + object: 'podman', + method: 'images_list', + params: [], + expect: { + data: [] + }, + filter: (images) => images + .map((image) => Image.getSingleton(image)) + .sort((imageA, imageB) => imageA.getDisplayTag().localeCompare(imageB.getDisplayTag())), + }), + }, + + /** + * Pod methods. + */ + pods: { + list: Model.declareRPC({ + object: 'podman', + method: 'pods_list', + params: [], + expect: { + data: [] + }, + filter: (pods) => pods + .map((pod) => Pod.getSingleton(pod)) + .sort((a, b) => a.getName().localeCompare(b.getName())) + }), + + create: Model.declareRPC({ + object: 'podman', + method: 'pod_create', + params: ['data'] + }), + }, + + /** + * Network management methods. + */ + networks: { + list: Model.declareRPC({ + object: 'podman', + method: 'networks_list', + params: [], + expect: { + data: [] + }, + filter: (networks) => networks + .map((network) => Network.getSingleton(network)) + .sort((networkA, networkB) => networkA.getName().localeCompare(networkB.getName())), + }), + + create: Model.declareRPC({ + object: 'podman', + method: 'network_create', + params: ['data'] + }), + }, + + /** + * Volume management methods. + */ + volumes: { + list: Model.declareRPC({ + object: 'podman', + method: 'volumes_list', + params: [], + expect: { + data: [] + }, + filter: (volumes) => volumes + .map((volume) => Volume.getSingleton(volume)) + .sort((volumeA, volumeB) => volumeA.getName().localeCompare(volumeB.getName())) + }), + + create: Model.declareRPC({ + object: 'podman', + method: 'volume_create', + params: ['data'] + }), + + import: Model.declareRPC({ + object: 'podman', + method: 'volume_import', + params: ['name', 'compressed'] + }), + }, + + /** + * Secret management methods. + */ + secrets: { + list: Model.declareRPC({ + object: 'podman', + method: 'secrets_list', + params: [], + expect: { + data: [] + }, + filter: (secrets) => secrets + .map((secret) => Secret.getSingleton(secret)) + .sort((secretA, secretB) => secretA.getName().localeCompare(secretB.getName())) + }), + + create: Model.declareRPC({ + object: 'podman', + method: 'secret_create', + params: ['name', 'data', 'labels'] + }), + }, + + /** + * System information methods. + */ + system: { + version: Model.declareRPC({ + object: 'podman', + method: 'version', + params: [] + }), + + info: Model.declareRPC({ + object: 'podman', + method: 'info', + params: [] + }), + + df: Model.declareRPC({ + object: 'podman', + method: 'system_df', + params: [] + }), + + prune: Model.declareRPC({ + object: 'podman', + method: 'system_prune', + params: ['all', 'volumes'] + }), + + debug: Model.declareRPC({ + object: 'podman', + method: 'system_debug', + params: [] + }) + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/ui.js b/luci-app-podman/htdocs/luci-static/resources/podman/ui.js new file mode 100644 index 000000000..3feff5391 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/ui.js @@ -0,0 +1,559 @@ +'use strict'; + +'require baseclass'; +'require ui'; +'require podman.constants as c'; + +const AbstractInputField = ui.Textfield.extend({ + __name__: 'Podman.UI.AbstractInputField', + + inputType: 'text', + + __init__(value, options) { + this.value = value; + this.options = Object.assign({ min: '', max: '' }, options); + }, + + render() { + const frameEl = E('div', { id: this.options.id }); + const inputEl = E('input', { + id: this.options.id ? `widget.${this.options.id}` : null, + name: this.options.name, + type: this.inputType, + class: 'cbi-input-text', + readonly: this.options.readonly ? '' : null, + disabled: this.options.disabled ? '' : null, + min: this.options.min, + max: this.options.max, + placeholder: this.options.placeholder, + value: this.value, + }); + + frameEl.appendChild(inputEl); + + return this.bind(frameEl); + }, +}); + +const UINumberfield = AbstractInputField.extend({ + __name__: 'Podman.UI.Numberfield', + inputType: 'number', +}); + +const UIDatefield = AbstractInputField.extend({ + __name__: 'Podman.UI.Datefield', + inputType: 'date', +}); + +const UIBase = baseclass.extend({ + __name__: 'Podman.UI', + + alert(text, type, timeLimited) { + timeLimited = timeLimited === true ? c.NOTIFICATION_TIMEOUT : timeLimited; + type = type || 'info'; + + if (Array.isArray(text) !== true) { + text = E('p', {}, text); + } + + if (parseInt(timeLimited) > 0) { + ui.addTimeLimitedNotification(type.toUpperCase(), text, timeLimited, type); + return; + } + + ui.addNotification(type.toUpperCase(), text, type); + }, + + showSpinningModal(title, text) { + ui.showModal(title, [E('div', { class: 'spinning' }, text)], 'loading-modal'); + }, +}); + +const UITooltip = baseclass.extend({ + __name__: 'Podman.UI.Tooltip', + + node: '', + tooltip: '', + options: {}, + + __init__(node, tooltip, options) { + this.node = node || ''; + this.tooltip = tooltip || ''; + this.options = options || {}; + }, + + render() { + const cssClass = 'cbi-tooltip-container'; + const cls = this.options.class ? `${this.options.class} ${cssClass}` : cssClass; + const options = Object.assign({}, this.options, { class: cls }); + + return E('div', options, [ + this.node, + E('div', { class: 'cbi-tooltip' }, this.tooltip), + ]); + } +}); + +const UISecretText = baseclass.extend({ + __name__: 'Podman.UI.SecretText', + + value: '', + censoredValue: '', + show: false, + + __init__(value, censoredValue) { + this.value = value; + this.censoredValue = censoredValue; + }, + + render() { + const me = this; + + return new UITooltip( + me.censoredValue, + _('Click to reveal/hide value'), + { + class: 'tooltip', + click() { + if (me.show) { + me.show = false; + this.childNodes[0].nodeValue = me.censoredValue; + + return; + } + + me.show = true; + this.childNodes[0].nodeValue = me.value; + }, + } + ).render(); + } +}); + +const UIButton = baseclass.extend({ + __name__: 'Podman.UI.Button', + + __init__(text, href, cssClass, tooltip) { + this.text = text; + this.href = href; + this.cssClass = cssClass; + this.tooltip = tooltip; + }, + + render() { + const attrs = { + class: this.cssClass ? 'cbi-button cbi-button-' + this.cssClass : 'cbi-button', + click: typeof this.href === 'function' ? this.href : (ev) => { + ev.preventDefault(); + window.location.href = this.href; + } + }; + + if (this.tooltip) { + attrs.title = this.tooltip; + } + + return E('button', attrs, this.text || ''); + } +}); + +const UIButtonNew = baseclass.extend({ + __name__: 'Podman.UI.ButtonNew', + + __init__(text, options) { + this.options = Object.assign({ + text: text || '', + // class: '' + type: '', + href: '', + click: () => { }, + tooltip: '', + }, options); + }, + + render() { + const click = this.options.href + ? ui.createHandlerFn(this, (event) => { + event.preventDefault(); + window.location.href = this.options.href; + }) + : this.options.click; + + const button = E('button', { + class: `cbi-button cbi-button-${this.options.type}`, + click, + }, this.options.text); + + if (this.options.tooltip) { + return new UITooltip(button, this.options.tooltip).render(); + } + + return button; + } +}); + +const UIJsonArea = baseclass.extend({ + __name__: 'Podman.UI.JsonArea', + + __init__(data) { + this.data = data || this.data; + }, + + render() { + const el = E('pre', { class: 'json-area' }); + const json = JSON.stringify(this.data, null, 2); + const nodes = []; + let last = 0; + json.replace( + /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, + (match, _p1, _p2, _p3, _p4, offset) => { + if (offset > last) + nodes.push(document.createTextNode(json.slice(last, offset))); + let cls = 'number'; + if (/^"/.test(match)) cls = /:$/.test(match) ? 'key' : 'string'; + else if (/true|false/.test(match)) cls = 'boolean'; + else if (/null/.test(match)) cls = 'null'; + nodes.push(E('span', { 'class': cls }, match)); + last = offset + match.length; + } + ); + if (last < json.length) + nodes.push(document.createTextNode(json.slice(last))); + for (const node of nodes) el.appendChild(node); + return el; + }, +}); + +const UIBashCodeArea = baseclass.extend({ + __name__: 'Podman.UI.BashCodeArea', + + code: '', + + __init__(code) { + this.code = code || this.code; + }, + + render() { + const el = E('pre', { class: 'code-area bash' }); + const code = this.code || ''; + const nodes = []; + let last = 0; + const bashRegex = /(#[^\n]*)|("(?:\\.|[^\\"])*"|'(?:\\.|[^\\'])*')|(\$\w+|\$\{[^}]+\})|\b(if|then|else|elif|fi|case|esac|for|while|do|done|in|break|continue|exit|return|stop_service|start_service)\b|(\b\d+\b)/g; + code.replace(bashRegex, (match, comment, string, variable, keyword, number, offset) => { + if (offset > last) + nodes.push(document.createTextNode(code.slice(last, offset))); + let cls = null; + if (comment) cls = 'comment'; + else if (string) cls = 'string'; + else if (variable) cls = 'variable'; + else if (keyword) cls = 'keyword'; + else if (number) cls = 'number'; + nodes.push(cls ? E('span', { 'class': cls }, match) : document.createTextNode(match)); + last = offset + match.length; + }); + if (last < code.length) + nodes.push(document.createTextNode(code.slice(last))); + for (const node of nodes) el.appendChild(node); + return el; + }, +}); + +/** + * Scrolling text log for streaming output. Each append() is one logical + * update: lines containing CR overwrite the last line (Podman pull progress + * uses CR to refresh "Copying blob X 50%"); plain lines append. Sticky + * auto-scroll: jumps to bottom on append unless the user scrolled up. + */ +const UIStreamLog = baseclass.extend({ + __name__: 'Podman.UI.StreamLog', + + maxLines: 2000, + autoScroll: true, + + __init__(opts) { + opts = opts || {}; + if (typeof opts.maxLines === 'number') this.maxLines = opts.maxLines; + if (typeof opts.autoScroll === 'boolean') this.autoScroll = opts.autoScroll; + this._container = E('div', { class: 'podman-stream-log' }); + this._lastLineNode = null; + this._lineCount = 0; + }, + + append(text) { + if (text == null) return this; + const stripped = String(text).replace(/\n+$/, ''); + if (!stripped) return this; + + const stick = this.autoScroll && this._isAtBottom(); + const cr = stripped.lastIndexOf('\r'); + const isProgress = cr >= 0; + const display = isProgress ? stripped.slice(cr + 1) : stripped; + + if (isProgress && this._lastLineNode) { + this._lastLineNode.textContent = display; + } else { + this._appendLine(display); + } + + if (stick) this._container.scrollTop = this._container.scrollHeight; + return this; + }, + + appendBlock(lines) { + if (Array.isArray(lines)) for (const l of lines) this.append(l); + return this; + }, + + clear() { + while (this._container.firstChild) this._container.removeChild(this._container.firstChild); + this._lastLineNode = null; + this._lineCount = 0; + return this; + }, + + render() { + return this._container; + }, + + _appendLine(text) { + const node = E('div', { class: 'log-line' }, [text || ' ']); + this._container.appendChild(node); + this._lastLineNode = node; + this._lineCount++; + while (this._lineCount > this.maxLines && this._container.firstChild) { + const removed = this._container.firstChild; + this._container.removeChild(removed); + this._lineCount--; + if (this._lastLineNode === removed) this._lastLineNode = this._container.lastChild; + } + }, + + _isAtBottom() { + const el = this._container; + return (el.scrollHeight - el.scrollTop - el.clientHeight) < 5; + } +}); + +/** + * Table builder with header and row chaining. + */ +const UITable = baseclass.extend({ + __name__: 'Podman.UI.Table', + + options: { class: 'table' }, + + headers: [], + rows: [], + + __init__(options) { + this.headers = []; + this.rows = []; + + const baseClass = this.options.class; + this.options = Object.assign({}, this.options, options); + if (options?.class) this.options.class = `${baseClass} ${options.class}`; + }, + + addHeader(header, options) { + this.headers.push({ + inner: header, + options: Object.assign({ class: 'th' }, options || {}), + }); + + return this; + }, + + setHeaders(headers) { + this.headers = headers; + + return this; + }, + + addRow(cells, options) { + this.rows.push({ + cells, + options: Object.assign({ class: 'tr' }, options || {}), + }); + + return this; + }, + + setRows(rows) { + this.rows = rows; + + return this; + }, + + render() { + let headerRow = ''; + + if (Array.isArray(this.headers) && this.headers.length > 0) { + headerRow = E('tr', { class: 'tr table-titles' }, + this.headers.map((header) => E('th', header.options, header.inner)) + ); + } + + let rows = ''; + + if (Array.isArray(this.rows) && this.rows.length > 0) { + rows = this.rows.map((row) => E('tr', row.options, + row.cells.map((cell) => E('td', Object.assign({ class: 'td' }, cell.options || {}), cell.inner)) + )); + } + + return E('table', this.options, [headerRow].concat(rows)); + } +}); + +const UITableList = UITable.extend({ + __name__: 'Podman.UI.TableList', + + options: { class: 'table table-list' }, + + addRow(label, value, rowOptions) { + return this.super('addRow', [[ + { inner: label }, + { inner: value } + ], rowOptions]); + }, +}); + +/** + * Tabbed interface using LuCI tabs API. + */ +const UITabs = baseclass.extend({ + __name__: 'Podman.UI.Tabs', + + tabs: [], + activeTab: null, + + __init__(activeTab) { + this.tabs = []; + this.activeTab = activeTab || null; + }, + + addTab(id, title, content, active) { + this.tabs.push({ + id, + title, + content, + active: active || false + }); + + return this; + }, + + render() { + const tabPanes = this.tabs.map((tab, index) => { + const isActive = tab.active || (!this.activeTab && index === 0) || (this.activeTab === tab.id); + + return E('div', { + class: 'tab-pane', + 'data-tab': tab.id, + 'data-tab-title': tab.title, + 'data-tab-active': isActive ? 'true' : null + }, [tab.content || E('div', { class: 'cbi-section' }, E('div', { class: 'cbi-section-node' }))]); + }); + + const tabContainer = E('div', { class: 'cbi-section' }, [ + E('div', { class: 'cbi-section-node' }, [ + E('div', { class: 'tab-panes' }, tabPanes) + ]) + ]); + + requestAnimationFrame(() => { + const panes = tabContainer.querySelectorAll('.tab-pane'); + ui.tabs.initTabGroup(panes); + }); + + return tabContainer; + } +}); + +const UIModal = baseclass.extend({ + __name__: 'Podman.UI.Modal', + + title: '', + content: null, + extraClasses: [], + + __init__(title, content, extraClasses) { + this.title = title || this.title; + this.content = content || this.content; + this.extraClasses = extraClasses || this.extraClasses; + }, + + render() { + ui.showModal(this.title, [ + E('div', {}, this.getContent()), + E('div', { class: 'd-flex justify-end mt-sm gap-5' }, this.getButtons()), + ], ...this.extraClasses); + }, + + getContent() { + return this.content; + }, + + getButtons() { + const buttons = []; + + if (typeof this.handleClose === 'function') { + buttons.push(this.getCloseButton()); + } + + if (typeof this.handleSubmit === 'function') { + buttons.push(this.getOkButton()); + } + + return buttons; + }, + + async handleClose() { + ui.hideModal(); + }, + + async handleSubmit() { + ui.hideModal(); + }, + + getOkButton() { + return new UIButtonNew(_('OK'), { + click: ui.createHandlerFn(this, 'handleSubmit'), + type: 'positive', + }).render(); + }, + + getCloseButton() { + return new UIButtonNew(_('Close'), { + click: ui.createHandlerFn(this, 'handleClose'), + type: 'negative', + }).render(); + }, +}); + +const PodmanUI = UIBase.extend({ + __name__: 'Podman.UI', + + Modal: UIModal, + + Button: UIButton, + ButtonNew: UIButtonNew, + // MultiButton: UIMultiButton, + + Numberfield: UINumberfield, + Datefield: UIDatefield, + + SecretText: UISecretText, + + JsonArea: UIJsonArea, + BashCodeArea: UIBashCodeArea, + + Table: UITable, + TableList: UITableList, + Tabs: UITabs, + Tooltip: UITooltip, + + StreamLog: UIStreamLog, +}); + +return PodmanUI; diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/utils.js b/luci-app-podman/htdocs/luci-static/resources/podman/utils.js new file mode 100644 index 000000000..9ca397edb --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/utils.js @@ -0,0 +1,238 @@ +'use strict'; + +'require baseclass'; + +const UtilIpv6 = baseclass.extend({ + /** + * Derive ULA IPv6 subnet and gateway from IPv4 subnet. + * Uses IPv4 octets 3-4 as subnet ID for consistent IPv4/IPv6 mapping. + * @param {string} ipv4 - IPv4 subnet in CIDR (e.g., "192.168.20.0/24") + * @param {string} ula_prefix - ULA prefix (e.g., "fd52:425:78eb::/48") + * @returns {{ipv6subnet: string, ipv6gateway: string}} IPv6 config with subnet/gateway + */ + deriveUlaFromIpv4(ipv4, ula_prefix) { + const ipv4Address = ipv4.split('/')[0]; + const octets = ipv4Address.split('.').map(Number); + const subnetIdHex = ((octets[2] << 8) | octets[3]).toString(16).padStart(4, '0'); + const ulaAddress = ula_prefix.split('/')[0]; + const ulaParts = ulaAddress.split('::'); + + let hextets = ulaParts[0].split(':'); + if (hextets.length === 1 && hextets[0] === "") { + hextets = []; + } + + while (hextets.length < 3) { + hextets.push('0'); + } + + const ipv6SubnetAddress = `${hextets.slice(0, 3).join(':')}:${subnetIdHex}::`; + + return { + ipv6subnet: `${ipv6SubnetAddress}/64`, + ipv6gateway: `${ipv6SubnetAddress}1` + }; + }, +}); + +const UtilIpv4 = baseclass.extend({ + // >>> 0 forces unsigned 32-bit interpretation (JS bitwise ops are signed) + _ipToInt(octets) { + return ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0; + }, + + _intToIp(n) { + return [(n >>> 24) & 0xFF, (n >>> 16) & 0xFF, (n >>> 8) & 0xFF, n & 0xFF].join('.'); + }, + + /** + * Convert a CIDR string to a start/end IP range. + * Works for both network addresses (10.89.0.0/24) and host addresses + * within a subnet (10.89.0.5/24) - the mask normalises either to the + * full network range. + * @param {string} cidr - CIDR notation (e.g., "10.89.0.128/25") + * @returns {{start_ip: string, end_ip: string}|null} Range or null if invalid + */ + cidrToRange(cidr) { + if (!cidr || typeof cidr !== 'string') return null; + + const parts = cidr.split('/'); + if (parts.length !== 2) return null; + + const octets = parts[0].split('.').map(Number); + const prefixLen = parseInt(parts[1], 10); + + if (octets.length !== 4 || octets.some(o => isNaN(o) || o < 0 || o > 255)) return null; + if (isNaN(prefixLen) || prefixLen < 0 || prefixLen > 32) return null; + + const ipInt = this._ipToInt(octets); + const mask = prefixLen === 0 ? 0 : (0xFFFFFFFF << (32 - prefixLen)) >>> 0; + + return { + start_ip: this._intToIp((ipInt & mask) >>> 0), + end_ip: this._intToIp((ipInt | (~mask >>> 0)) >>> 0), + }; + }, + + /** + * Return the first usable host address in a subnet (network address + 1). + * Conventionally used as the gateway address. + * @param {string} cidr - CIDR notation (e.g., "10.89.0.0/24") + * @returns {string|null} First host IP (e.g., "10.89.0.1") or null if invalid + */ + firstHost(cidr) { + const range = this.cidrToRange(cidr); + if (!range) return null; + + const octets = range.start_ip.split('.').map(Number); + const next = (this._ipToInt(octets) + 1) >>> 0; + + return this._intToIp(next); + }, +}); + +const UtilFormat = baseclass.extend({ + __name__: 'Podman.Util.Format', + + /** + * Format bytes to human-readable size + */ + bytes(bytes, decimals) { + if (!bytes) return '0 B'; + + decimals = decimals ?? 2; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / k ** i).toFixed(decimals)) + ' ' + sizes[i]; + }, + + /** + * Format timestamp to locale string with leading zeros + */ + date(timestamp) { + if (!timestamp) { + return _('Never'); + } + + let date; + + if (typeof timestamp === 'string') { + if (timestamp === '0001-01-01T00:00:00Z' || timestamp.startsWith('0001-')) { + return _('Never'); + } + date = new Date(timestamp); + } else { + date = new Date(timestamp * 1000); + } + + if (isNaN(date.getTime())) { + return _('Unknown'); + } + + if (date.getFullYear() < 1970) { + return _('Never'); + } + + const day = ('0' + date.getDate()).slice(-2); + const month = ('0' + (date.getMonth() + 1)).slice(-2); + const year = date.getFullYear(); + const hours = ('0' + date.getHours()).slice(-2); + const minutes = ('0' + date.getMinutes()).slice(-2); + const seconds = ('0' + date.getSeconds()).slice(-2); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; + }, + + /** + * Parse memory string to bytes + * Supports formats: 512m, 1g, 2gb, 1024, etc. + * @param {string} memStr - Memory string (e.g., "512m", "1g", "2gb") + * @returns {number} Size in bytes + */ + parseMemory(memStr) { + if (!memStr) return 0; + + const match = memStr.match(/^(\d+(?:\.\d+)?)\s*([kmgt]?b?)?$/i); + if (!match) return 0; + + const value = parseFloat(match[1]); + const unit = (match[2] || 'b').toLowerCase(); + + const multipliers = { + b: 1, + k: 1024, + kb: 1024, + m: 1024 * 1024, + mb: 1024 * 1024, + g: 1024 * 1024 * 1024, + gb: 1024 * 1024 * 1024, + t: 1024 * 1024 * 1024 * 1024, + tb: 1024 * 1024 * 1024 * 1024 + }; + + return Math.floor(value * (multipliers[unit] || 1)); + }, + + /** + * Parse duration string to nanoseconds (Podman format) + * Supports formats: 30s, 1m, 1h, 500ms, etc. + * @param {string} duration - Duration string (e.g., "30s", "1m", "1h") + * @returns {number} Duration in nanoseconds, or 0 if invalid + */ + parseDuration(duration) { + if (!duration) return 0; + + const match = duration.match(/^(\d+(?:\.\d+)?)\s*(ns|us|ms|s|m|h)$/); + if (!match) return 0; + + const value = parseFloat(match[1]); + const unit = match[2]; + + const multipliers = { + ns: 1, + us: 1000, + ms: 1000000, + s: 1000000000, + m: 60000000000, + h: 3600000000000 + }; + + return Math.floor(value * (multipliers[unit] || 0)); + }, +}); + +/** + * Shared utility functions. + */ +return baseclass.extend({ + ipv4: new UtilIpv4(), + + ipv6: new UtilIpv6(), + + format: new UtilFormat(), + + truncate(str, maxLength) { + if (!str || str.length <= maxLength) { + return str; + } + + return str.substring(0, maxLength) + '...'; + }, + + moveArrayItem(arr, fromIndex, toIndex) { + const [item] = arr.splice(fromIndex, 1); + arr.splice(toIndex, 0, item); + return arr; + }, + + _p(stringPlural) { + return this._n(2, '', stringPlural); + }, + + _n(length, stringSingular, stringPlural) { + return parseInt(length) === 1 ? + stringSingular : stringPlural; + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/podman/view.js b/luci-app-podman/htdocs/luci-static/resources/podman/view.js new file mode 100644 index 000000000..9b1fdd103 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/podman/view.js @@ -0,0 +1,387 @@ +'use strict'; + +'require baseclass'; +'require view'; +'require form'; +'require dom'; +'require ui'; + +'require podman.ui as podmanUI'; +'require podman.constants as constant'; + +const ViewDefault = { + scrollToTop() { + document.body.scrollTop = 0; + document.documentElement.scrollTop = 0; + }, + + loading(text) { + this.scrollToTop(); + podmanUI.showSpinningModal(null, text || _('Loading...')); + }, + + confirm(content, onConfirm) { + const modal = new podmanUI.Modal(_('Confirm'), content, [ 'confirm-modal' ]); + modal.getCloseButton = () => new podmanUI.ButtonNew(_('Cancel'), { + click: () => ui.hideModal(), + type: 'negative', + }).render(); + modal.handleSubmit = onConfirm; + modal.render(); + }, + + error(message) { + this.scrollToTop(); + ui.hideModal(); + podmanUI.alert(message, 'error'); + }, + + warning(message) { + this.scrollToTop(); + ui.hideModal(); + podmanUI.alert(message, 'warning', true); + }, + + success(message) { + this.scrollToTop(); + ui.hideModal(); + podmanUI.alert(message, 'success', true); + }, +}; + +const ViewBase = view.extend({ + ...ViewDefault, + __name__: 'PodmanView.Base', + + handleSaveApply: null, + handleSave: null, + handleReset: null, + + __init__() { + document.querySelector('head').appendChild(E('link', { + rel: 'stylesheet', + type: 'text/css', + href: L.resource('view/podman/podman.css'), + })); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') ui.hideModal(); + }); + + this.super('__init__', []); + }, +}); + +const ViewList = ViewBase.extend({ + __name__: 'Podman.View.List', + + map: null, + section: null, + title: '', + sectionName: '', + sectionHeadline: '', + TableSelectSection: null, + + async render(data) { + const listData = {}; + listData[this.sectionName] = data; + + await this.initSection(listData); + await this.updateSection(); + + return this.transformMap(await this.map.render()); + }, + + transformMap(map) { + return map; + }, + + initTableSelectSection(data) { + this.map = new form.JSONMap(data, this.title); + + this.section = this.map.section( + this.TableSelectSection, + this.sectionName, + '', + this.sectionHeadline || _('Manage podman %s').format(this.title.toLowerCase()) + ); + this.section.view = this; + this.section.anonymous = true; + this.section.footer = true; + + return this.section; + }, + + async initSection(data) { + if (!this.TableSelectSection) { + // Require on-the-fly to avoid circular dependency + const mod = await L.require('podman.TableSelectSection'); + this.TableSelectSection = mod.TableSelectSection; + } + return this.initTableSelectSection(data); + }, + + getSection() { + return this.section; + }, +}); + +/** + * Form View + */ +const ViewForm = baseclass.extend({ + ...ViewDefault, + __name__: 'Podman.View.Form', + + map: null, + section: null, + + __init__() { + + }, + + makeData() { + return {}; + }, + + async render() { + const data = this.makeData(); + const sectionName = Object.keys(data)[0]; + this.map = new form.JSONMap(data, '', ''); + this.section = this.map.section(form.TypedSection, sectionName, ''); + this.section.anonymous = true; + + await this.createForm(); + + return this.map.render(); + }, + + createForm() {}, + + getField(name) { + return this.section?.getOption(name); + }, + + getFields() { + return this.section?.getOption() || {}; + }, + + getFieldValue(name) { + return this.getField(name)?.formvalue(this.section?.sectiontype); + }, + + getFieldValues() { + const values = {}; + const fields = this.getFields(); + + Object.keys(fields).forEach((name) => { + values[name] = fields[name].formvalue(this.section?.sectiontype); + }); + + return values; + }, + + isValid() { + const fields = this.getFields(); + const sectionId = this.section.sectiontype; + + for (const [_key, field] of Object.entries(fields)) { + if (field.isValid(sectionId) === false) { + return false; + } + } + + return true; + }, + + scrollToInvalid() { + const invalidElement = this.map.root.querySelector('.cbi-input-invalid'); + invalidElement.scrollIntoView(); + }, + + async save() { + try { + await this.map.save(() => {}, true); + } catch (error) { + return false; + } + + return true; + }, + + async handleCreate(createFn, title, textLoading, textSuccess) { + ui.hideModal(); + + const loadingText = textLoading || _('Creating %s').format(title); + const successText = textSuccess || _('%s created successfully').format(title); + + this.loading(loadingText); + + await createFn(); + ui.hideModal(); + if (successText !== false) { + this.success(successText); + } + }, +}); + +const ViewTabs = ViewBase.extend({ + __name__: 'Podman.View.Tabs', + tabs: null, + + __init__() { + this.tabs = new podmanUI.Tabs('info'); + this.super('__init__', []); + }, + + renderTab(tab, content, description) { + const tabContainer = document.querySelector(`.tab-pane[data-tab="${tab}"]`); + const tabContainerNode = tabContainer?.querySelector('.cbi-section-node'); + + if (!tabContainerNode) return; + + if (description) { + tabContainer.insertBefore(E('div', { + class: 'cbi-section-descr' + }, description), tabContainer.firstChild); + } + + dom.content(tabContainerNode, content); + }, + + getTabInstance(name) { + const tabNode = document.querySelector(`.tab-pane[data-tab="${name}"]`); + return tabNode ? dom.findClassInstance(tabNode) : null; + }, +}); + +const ViewContainer = ViewTabs.extend({ + __name__: 'Podman.View.Container', + listUrl: '#', + + async render() { + window.addEventListener('pagehide', () => this.stopStreams(), { once: true }); + + return E('div', {}, [ this.createHeader(), this.tabs.render() ]); + }, + + createHeader(name, isRunning, isStopped, isPaused) { + return E('div', { class: 'mb-sm container-toolbar' }, [ + E('div', { class: 'd-flex align-start' }, [ + E('h2', { class: 'mb-sm' }, [ name ]), + new podmanUI.ButtonNew(constant.ICON.BACK, { + click: () => this.redirectToList(), + type: 'none', + }).render(), + ]), + E('div', { class: 'd-flex align-center' }, [ + new podmanUI.ButtonNew(constant.ICON.START, { + click: ui.createHandlerFn(this, 'handleStart'), + type: isRunning ? 'active' : '', + }).render(), + new podmanUI.ButtonNew(constant.ICON.STOP, { + click: ui.createHandlerFn(this, 'handleStop'), + type: isStopped ? 'active' : '', + }).render(), + new podmanUI.ButtonNew(constant.ICON.RESTART, { + click: ui.createHandlerFn(this, 'handleRestart'), + }).render(), + new podmanUI.ButtonNew(constant.ICON.PAUSE, { + click: ui.createHandlerFn(this, 'handlePause'), + type: isPaused ? 'active' : '', + }).render(), + new podmanUI.ButtonNew(_('Delete'), { + click: ui.createHandlerFn(this, 'handleRemove'), + type: 'negative', + }).render(), + ]), + ]); + }, + + redirectToList() { + this.stopStreams(); + window.location.href = this.listUrl; + }, + + stopStreams() { + }, +}); + +const ViewTabContent = baseclass.extend({ + ...ViewDefault, + __name__: 'PodmanView.TabContent', + tab: null, + active: false, + + __init__(tab) { + this.tab = tab || this.tab; + this.super('__init__', []); + + if (!this.tab) { + return; + } + + let attempts = 0; + new Promise((resolve, reject) => { + const check = () => { + if (attempts++ > 100) { reject(); return; } + const element = document.querySelector(`[data-tab="${this.tab}"]`); + if (element) resolve(element); + else setTimeout(check, 100); + }; + check(); + }).then(() => { + const mainNode = document.querySelector(`.tab-pane[data-tab="${this.tab}"]`); + + dom.bindClassInstance(mainNode, this); + + const mutationObserver = new MutationObserver((mutationsList) => { + let handled = false; + mutationsList.forEach((mutation) => { + const setActive = mutation.target.dataset.tabActive === 'true'; + + if (handled || mutation.attributeName !== 'data-tab-active') return; + handled = true; + + if (this.active === setActive) { + return; + } + + if (setActive) { + this.active = true; + this.onTabActive(); + return; + } + + this.active = false; + this.onTabInactive(); + }); + }); + + mutationObserver.observe(mainNode, { attributes: true }); + }).catch(() => {}); + }, + + async onTabActive() { }, + + async onTabInactive() { }, + + renderTabContent(headline, content) { + return E('div', {}, [ + E('h4', {}, headline), + ...(content || []), + ]); + }, + + warningContent(message) { + return E('div', { class: 'alert-message d-flex justify-center p-sm' }, message); + }, +}); + +return baseclass.extend({ + base: ViewBase, + list: ViewList, + form: ViewForm, + container: ViewContainer, + tabContent: ViewTabContent, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/info.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/info.js new file mode 100644 index 000000000..09fc01ff6 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/info.js @@ -0,0 +1,388 @@ +'use strict'; + +'require ui'; + +'require podman.ui as podmanUI'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; +'require podman.rpc as podmanRPC'; +'require podman.utils as utils'; + +'require podman.model.Container as Container'; + +const CENSORED_VALUE = '••••••••'; + +return podmanView.tabContent.extend({ + tab: 'info', + + container: null, + initScriptStatus: '', + + async render(container) { + this.container = container; + + const sections = await Promise.all([ + this.basicSection(), + this.configSection(), + this.networkSection(), + this.envSection(), + this.mountSection(), + ]); + + this.updateInitScriptIcon(sections[0]); + + return sections; + }, + + basicSection() { + const basicTable = new podmanUI.TableList(); + + const editableNameField = new ui.Textfield(this.container.getName(), { name: 'container-name' }).render(); + const editableName = new podmanForm.EditableField('container-name', editableNameField); + editableName.onSubmit = (value) => this.handleUpdateName(value); + + const policies = { + 'no': _('No'), + 'always': _('Always'), + 'on-failure': _('On Failure'), + 'unless-stopped': _('Unless Stopped') + }; + const currentPolicy = this.container.getRestartPolicyName(); + const editableRestartField = new ui.Select(currentPolicy, policies, { name: 'container-restart' }).render(); + const editableRestart = new podmanForm.EditableField('container-restart', editableRestartField); + editableRestart.onSubmit = (value) => this.handleUpdateRestartPolicy(value); + + basicTable + .addRow(_('Name'), editableName.render()) + .addRow(_('ID'), this.container.getID().substring(0, 64)) + .addRow(_('Image'), new podmanUI.Tooltip(this.container.getImageName(), this.container.Image, { class: 'tooltip' }).render()) + .addRow(_('Tag'), this.container.getImageTag()) + .addRow(_('Status'), this.container.getStateBadge()) + .addRow(_('Created'), this.container.getCreated(true)) + .addRow(_('Started'), this.container.getStartedAt(true)) + .addRow(_('Restart Policy'), editableRestart.render()) + .addRow(_('Auto Update'), this.container.getAutoUpdateLabel() || _('Disabled')) + .addRow(_('Init Script'), E('div', { + class: `init-script-form align-center ${this.container.getRestartPolicyName() === 'no' ? 'd-none' : 'd-flex'}` + }, [ + new podmanUI.Tooltip( + new podmanUI.ButtonNew( + this.container.getAutoStartStatusIcon(), + { + click: ui.createHandlerFn(this, 'handleToggleInitScript'), + } + ).render(), + _('Click to enable/disable'), + { + class: 'cursor-pointer', + } + ).render(), + + new podmanUI.ButtonNew(_('Show'), { + type: 'none', + click: ui.createHandlerFn(this, 'handleShowInitScript'), + }).render(), + new podmanUI.ButtonNew(_('Generate'), { + type: 'none', + click: ui.createHandlerFn(this, 'handleGenerateInitScript'), + }).render(), + ])) + ; + + return E('div', {}, [ + E('h4', {}, _('Basic Information')), + basicTable.render(), + ]); + }, + + configSection() { + const configTable = new podmanUI.TableList(); + const cmd = this.container.getCmdString() || '-'; + const entrypoint = this.container.getEntrypointString() || '-'; + const createCommand = this.container.getCreateCommandString() || '-'; + const createCommandField = new podmanUI.SecretText(createCommand, CENSORED_VALUE); + + configTable + .addRow(_('Create Command'), createCommandField.render()) + .addRow(_('Command'), cmd) + .addRow(_('Entrypoint'), entrypoint) + .addRow(_('Working Directory'), this.container.getWorkingDir()) + .addRow(_('User'), this.container.getUser()) + .addRow(_('Hostname'), this.container.getHostname()) + .addRow(_('Privileged'), this.container.getPrivileged() ? _('Yes') : _('No')) + .addRow(_('TTY'), this.container.getTty() ? _('Yes') : _('No')) + .addRow(_('Interactive'), this.container.getInteractive() ? _('Yes') : _('No')) + ; + + return E('div', {}, [ + E('h4', {}, _('Configuration')), + configTable.render(), + ]); + }, + + async networkSection () { + const table = new podmanUI.TableList(); + const { NetworkMode, Links } = this.container.getHostConfig(); + const { Networks, IPAddress } = this.container.getNetworkSettings(); + + table.addRow(_('Network Mode'), NetworkMode || _('default')); + + const systemNetworks = ['bridge', 'host', 'none', 'container', 'slirp4netns']; + let userNetworks = []; + + if (Networks && Object.keys(Networks).length > 0) { + userNetworks = Object.keys(Networks).filter((netName) => !systemNetworks.includes(netName)); + + userNetworks.forEach((netName) => { + const net = Networks[netName]; + const tooltip = (() => { + if (!this.container.isRunning()) { + return _('Container is not running'); + } + + const parts = []; + if (net.IPAddress) parts.push(`IPv4: ${net.IPAddress}`); + if (net.GlobalIPv6Address) parts.push(`IPv6: ${net.GlobalIPv6Address}`); + else parts.push('IPv6: disabled'); + if (net.Gateway) parts.push(`Gateway: ${net.Gateway}`); + if (net.MacAddress) parts.push(`MAC: ${net.MacAddress}`); + return parts.join('\n'); + })(); + + const ipContent = net.GlobalIPv6Address + ? [net.IPAddress || '-', E('br'), net.GlobalIPv6Address] + : net.IPAddress || '-'; + + table.addRow( + new podmanUI.Tooltip(E('span', { class: 'tooltip' }, netName), tooltip).render(), + E('div', { class: 'd-flex align-center' }, [ + E('span', {}, ipContent), + new podmanUI.Tooltip('✖', _('Click to disconnect'), { + class: 'text-error cursor-pointer container-icon-disconnect ml-sm border-0', + click: () => this.handleNetworkDisconnect(netName), + }).render(), + ]) + ); + }); + } + + if (IPAddress && userNetworks.length === 0) { + table.addRow(_('IP Address'), IPAddress); + } + + const podmanNetworks = await podmanRPC.networks.list(); + const networkOptions = Object.fromEntries(podmanNetworks.map(item => [item.name, item.name])); + + const networkSelectWidget = new ui.Select([''], networkOptions, { name: 'container-connect-network', optional: false }); + const ipTextWidget = new ui.Textfield('', { name: 'container-connect-ip', placeholder: '192.168.1.100', datatype: 'ip4addr' }); + + table.addRow(_('Connect to'), [ + E('div', { class: 'd-flex align-center container-connect' }, [ + networkSelectWidget.render(), + ipTextWidget.render(), + new podmanUI.Button(_('Connect'), () => { + const selectedNetwork = networkSelectWidget.getValue(); + const selectedIp = ipTextWidget.getValue(); + + networkSelectWidget.triggerValidation(); + ipTextWidget.triggerValidation(); + + if (!selectedNetwork || !ipTextWidget.isValid()) { + return; + } + + this.handleNetworkConnect(selectedNetwork, selectedIp); + }).render(), + ]), + ]); + + table.addRow(_('Ports'), E('div', {}, + this.container.getPorts().map((port) => E('div', {}, port.string)), + )); + + if (Links && Links.length > 0) { + const linkNodes = Links.reduce((nodes, link, i) => { + if (i > 0) nodes.push(E('br')); + nodes.push(link); + return nodes; + }, []); + table.addRow(_('Links'), linkNodes); + } + + return E('div', { class: 'networks' }, [ + E('h4', {}, _('Network')), + table.render(), + ]); + }, + + envSection() { + const table = new podmanUI.Table(); + table + .addHeader(_('Variable'), { style: 'width: 50%;' }) + .addHeader(_('Value'), { style: 'width: 50%;' }); + + this.container.getEnvironmentVars().forEach((env) => { + const parts = env.split('='); + const varName = parts[0]; + const varValue = parts.slice(1).join('='); + const valueField = new podmanUI.SecretText(varValue, CENSORED_VALUE); + + table.addRow([{ + inner: varName + }, + { + inner: valueField.render(), + }, + ]); + }); + + return E('div', {}, [ + E('h4', {}, _('Environment Variables')), + table.render(), + ]); + }, + + mountSection() { + const table = new podmanUI.Table(); + table + .addHeader(_('Type'), { style: 'width: 10%;' }) + .addHeader(_('Source'), { style: 'width: 35%;' }) + .addHeader(_('Destination'), { style: 'width: 35%;' }) + .addHeader(_('Mode'), { style: 'width: 10%;' }) + ; + + const truncatedField = (value) => { + const v = value || '-'; + return v.length > 50 ? new podmanUI.Tooltip(utils.truncate(v, 50), v).render() : v; + }; + + this.container.getMounts().forEach((mount) => { + const mountSourceField = truncatedField(mount.Source); + const mountDestField = truncatedField(mount.Destination); + + table.addRow([ + { inner: mount.Type || '-' }, + { inner: mountSourceField }, + { inner: mountDestField }, + { inner: mount.RW ? 'rw' : 'ro' }, + ]); + }) + + return E('div', {}, [ + E('h4', {}, _('Mounts')), + table.render(), + ]); + }, + + async _refreshContainer() { + const inspectData = await this.container.inspect(); + this.container = Container.getSingleton(inspectData); + }, + + async _refreshNetworkSection() { + const networkSection = await this.networkSection(); + document.querySelector('.networks').replaceWith(networkSection); + }, + + async updateInitScriptIcon(infoSection) { + this.initScriptStatus = await this.container.checkInitScript(); + const initScriptIconElement = (infoSection || document).querySelector('.autostart-status'); + const initScriptIcon = this.container.getAutoStartStatusIcon(this.initScriptStatus); + initScriptIconElement.replaceWith(initScriptIcon); + }, + + async handleUpdateName(name) { + if (!name || this.container.Name === name) { + return; + } + + this.loading(_('Update name')); + + await this.container.updateName(name); + + document.querySelector('.container-toolbar h2').textContent = name; + this.success(_('Name updated successfully')); + }, + + async handleUpdateRestartPolicy(policy) { + this.loading(_('Update restart policy')); + + const updateData = { RestartPolicy: policy }; + if (policy === 'on-failure') updateData.RestartRetries = 5; + + await this.container.update(updateData); + await this._refreshContainer(); + + const initScriptForm = document.querySelector('.init-script-form'); + if (policy === 'no') { + initScriptForm.classList.replace('d-flex', 'd-none'); + } else { + initScriptForm.classList.replace('d-none', 'd-flex'); + } + + this.updateInitScriptIcon(); + + this.success(_('Restart policy updated successfully')); + }, + + async handleNetworkConnect(networkName, ip) { + this.loading(_('Connecting container to network')); + + const params = {}; + if (ip) params.static_ips = [ip]; + + await this.container.connect(networkName, params); + await this._refreshContainer(); + this.success(_('Connected successfully to %s').format(networkName)); + await this._refreshNetworkSection(); + }, + + async handleNetworkDisconnect(networkName) { + this.loading(_('Disconnect container from network')); + + await this.container.disconnect(networkName); + await this._refreshContainer(); + this.success(_('Disconnected from %s').format(networkName)); + await this._refreshNetworkSection(); + }, + + async handleGenerateInitScript() { + this.loading(_('Creating auto-start configuration for %s').format(this.container.getName())); + + await this.container.generateInitScript(); + await this.container.enableInitScript(); + ui.hideModal(); + window.location.reload(); + }, + + async handleShowInitScript() { + this.loading(_('Loading init script')); + + const result = await this.container.showInitScript(); + ui.hideModal(); + + if (!result || !result.path || !result.content) return; + + const modal = new podmanUI.Modal(result.path, [ + new podmanUI.BashCodeArea(result.content.replace(/\\n/g, '\n')).render() + ]); + modal.handleSubmit = undefined; + modal.render(); + }, + + async handleToggleInitScript() { + if (this.initScriptStatus === 'missing') { + return this.handleGenerateInitScript(); + } else if (this.initScriptStatus === 'enabled') { + this.loading(_('Disabling init script')); + await this.container.disableInitScript(); + ui.hideModal(); + window.location.reload(); + } else if (this.initScriptStatus === 'disabled') { + this.loading(_('Enabling init script')); + await this.container.enableInitScript(); + ui.hideModal(); + window.location.reload(); + } + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/logs.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/logs.js new file mode 100644 index 000000000..01703ea2d --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/logs.js @@ -0,0 +1,137 @@ +'use strict'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; + +const TIMESTAMP_RE_GLOBAL = /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\s*/g; +const ANSI_REGEX = /\x1B\[[0-9;]*[A-Za-z]/g; +const MAX_LOG_LINES = 1000; + +return podmanView.tabContent.extend({ + tab: 'logs', + container: null, + + render(container) { + this.container = container; + + this.logViewer = E('pre', { id: 'log-viewer', class: 'terminal-area' }, []); + + return this.renderTabContent('', [ + this.renderLogToolbar(), + this.logViewer, + ]); + }, + + onTabActive() { + // Running: auto-start live stream. Stopped: load historical logs on demand via play button. + if (this.container.isRunning()) { + this.startStream(); + } + }, + + onTabInactive() { + this.stopStream(); + }, + + _setStreamActive(active) { + this.logLinesInput.disabled = active; + this.logSinceInput.disabled = active; + this.logUntilInput.disabled = active; + this.playButton.querySelector('.cbi-button').classList.toggle('cbi-button-active', active); + this.stopButton.querySelector('.cbi-button').classList.toggle('cbi-button-active', !active); + }, + + startStream() { + if (!this.container || this.logsStream) { + return; + } + + this._setStreamActive(true); + + this.logViewer.textContent = ''; + + const toUnixSince = (val) => val ? new Date(val + 'T00:00:00').getTime() / 1000 : null; + const toUnixUntil = (val) => val ? new Date(val + 'T23:59:59').getTime() / 1000 : null; + + this.logsStream = this.container.streamLogs((data) => { + if (!data || !data.raw) return; + + const line = data.raw + .replace(TIMESTAMP_RE_GLOBAL, '') + .replace(ANSI_REGEX, '') + .trim(); + + if (!line) return; + + this.logViewer.textContent += line + '\n'; + this.trimLogOutput(); + this.logViewer.scrollTop = this.logViewer.scrollHeight; + }, { + tail: this.logLinesInput.value || 100, + since: toUnixSince(this.logSinceInput.value), + until: toUnixUntil(this.logUntilInput.value), + follow: this.container.isRunning(), + }); + }, + + stopStream() { + if (!this.logsStream) return; + + this._setStreamActive(false); + + this.logsStream.stop(); + this.logsStream = null; + }, + + renderLogToolbar() { + // Fields start disabled only for running containers (stream auto-starts and locks them). + // For stopped containers they start enabled so the user can set filters before fetching. + const initialDisabled = this.container.isRunning(); + const linesField = new podmanUI.Numberfield(100, { id: 'log-lines', min: '10', max: '250', disabled: initialDisabled }).render(); + const sinceField = new podmanUI.Datefield(null, { id: 'log-since', disabled: initialDisabled }).render(); + const untilField = new podmanUI.Datefield(null, { id: 'log-until', disabled: initialDisabled }).render(); + + this.logLinesInput = linesField.querySelector('input'); + this.logSinceInput = sinceField.querySelector('input'); + this.logUntilInput = untilField.querySelector('input'); + + this.stopButton = new podmanUI.ButtonNew('◼', { + click: () => this.stopStream(), + type: ' stop-button', + tooltip: _('Stop stream'), + }).render(); + + this.playButton = new podmanUI.ButtonNew('►', { + click: () => this.startStream(), + type: 'active play-button', + tooltip: _('Start stream'), + }).render(); + + return E('div', { class: 'd-flex align-center mb-sm' }, [ + E('label', { class: 'mr-xs' }, _('Number of lines')), + linesField, + '', + E('label', { class: 'ml-xs mr-xs' }, _('Since')), + sinceField, + + E('label', { class: 'ml-xs mr-xs' }, _('Until')), + untilField, + + E('div', { class: 'ml-xs' }, ''), + + new podmanUI.ButtonNew('🗑️', { + click: () => this.logViewer.textContent = '', + tooltip: _('Clear logs'), + }).render(), + this.stopButton, + this.playButton, + ]); + }, + + trimLogOutput() { + const lines = this.logViewer.textContent.split('\n'); + if (lines.length > MAX_LOG_LINES) { + this.logViewer.textContent = lines.slice(lines.length - MAX_LOG_LINES).join('\n'); + } + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/processes.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/processes.js new file mode 100644 index 000000000..a691da6e5 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/processes.js @@ -0,0 +1,99 @@ +'use strict'; + +'require dom'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; + +return podmanView.tabContent.extend({ + tab: 'ps', + container: null, + + render(container) { + this.container = container; + + if (!this.container.isRunning()) { + return this.warningContent(_('Container is not running')); + } + + this.tableContent = E('div', { class: 'ps-table-content' }, []); + + return this.renderTabContent('', [ + this.tableContent, + ]); + }, + + onTabActive() { + if (!this.container || !this.container.isRunning() || this.processStream) { + return; + } + + this.processStream = this.container.streamTop((ps) => { + this.updateProcessList(ps); + }); + }, + + onTabInactive() { + if (!this.processStream) return; + + this.processStream.stop(); + this.processStream = null; + }, + + updateProcessList(ps) { + if (!ps || !ps.Titles || !ps.Processes || ps.Titles.length === 0) { + return; + } + + const psTable = new podmanUI.Table(); + const columnWidth = `width: ${100 / ps.Titles.length}%;`; + const timeIndex = ps.Titles.indexOf('ELAPSED'); + + ps.Titles.forEach((title) => { + psTable.addHeader(_(title), { style: columnWidth }); + }); + + ps.Processes.forEach((process) => { + psTable.addRow(process.map((detail, i) => ({ + inner: i === timeIndex ? this._formatElapsedTime(detail) : detail + }))); + }); + + dom.content(this.tableContent, psTable.render()); + }, + + _formatElapsedTime(timeStr) { + if (!timeStr) return '-'; + + const result = []; + const pattern = /(\d+(?:\.\d+)?)([ydhms])/g; + let match; + + while ((match = pattern.exec(timeStr)) !== null) { + let value = parseFloat(match[1]); + const unit = match[2]; + + if (unit === 's' && value > 1000) { + value = Math.floor(value / 1000000000); + + if (value >= 60) { + const mins = Math.floor(value / 60); + const secs = value % 60; + if (mins > 0) result.push(`${mins}m`); + if (secs > 0) result.push(`${secs}s`); + continue; + } + } else { + value = Math.floor(value); + } + + if (unit === 's' || unit === 'm') { + result.push(String(value).padStart(2, '0') + unit); + } else { + result.push(value + unit); + } + } + + return result.length > 0 ? result.join('') : timeStr; + } +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/stats.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/stats.js new file mode 100644 index 000000000..0aff2a19c --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/container-tab/stats.js @@ -0,0 +1,107 @@ +'use strict'; + +'require dom'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.utils as podmanUtil'; + +return podmanView.tabContent.extend({ + tab: 'stats', + container: null, + + render(container) { + this.container = container; + + if (!this.container.isRunning()) { + return this.warningContent(_('Container is not running')); + } + + return this.renderTabContent(_('Statistics'), [ + this.renderStatsTable(), + ]); + }, + + onTabActive() { + if (!this.container || !this.container.isRunning() || this.statsStream) { + return; + } + + this.statsStream = this.container.streamStats((stats) => { + this.updateStatsDisplay(stats); + }); + }, + + onTabInactive() { + if (!this.statsStream) { + return; + } + + this.statsStream.stop(); + this.statsStream = null; + }, + + renderStatsTable() { + const table = new podmanUI.TableList(); + + table + .addRow(_('CPU Usage'), '-', { 'data-stat': 'cpu' }) + .addRow(_('Memory Usage'), '-', { 'data-stat': 'memory' }) + .addRow(_('Memory Limit'), '-', { 'data-stat': 'memory-limit' }) + .addRow(_('Memory %'), '-', { 'data-stat': 'memory-percent' }) + .addRow(_('Network I/O'), '-', { 'data-stat': 'network-io' }) + .addRow(_('Block I/O'), '-', { 'data-stat': 'block-io' }) + .addRow(_('PIDs'), '-', { 'data-stat': 'pids' }) + ; + + return table.render(); + }, + + updateStatsDisplay(stats) { + if (!stats) return; + + if (!this.statElements) { + this.statElements = {}; + for (const key of ['cpu', 'memory', 'memory-limit', 'memory-percent', 'network-io', 'block-io', 'pids']) { + this.statElements[key] = document.querySelector(`[data-stat="${key}"] td:last-of-type`); + } + } + + const updates = [ + ['cpu', stats.CPU != null ? stats.CPU.toFixed(2) + '%' : '-'], + ['memory', podmanUtil.format.bytes(stats.MemUsage) || '-'], + ['memory-limit', podmanUtil.format.bytes(stats.MemLimit) || '-'], + ['memory-percent', stats.MemPerc != null ? stats.MemPerc.toFixed(2) + '%' : '-'], + ['network-io', this._formatNetworkIO(stats.Network)], + ['block-io', this._formatBlockIO(stats.BlockInput, stats.BlockOutput)], + ['pids', stats.PIDs], + ]; + + for (const [key, value] of updates) { + if (this.statElements[key]) + dom.content(this.statElements[key], value); + } + }, + + _formatNetworkIO(networks) { + const parts = Object.keys(networks || {}).map((iface) => { + const net = networks[iface]; + return `${iface}: ↓ ${podmanUtil.format.bytes(net.RxBytes)} / ↑ ${podmanUtil.format.bytes(net.TxBytes)}`; + }); + + if (parts.length === 0) return '-'; + + return parts.reduce((nodes, part, i) => { + if (i > 0) nodes.push(E('br')); + nodes.push(part); + return nodes; + }, []); + }, + + _formatBlockIO(blockInput, blockOutput) { + return _('Read: %s / Write: %s').format( + podmanUtil.format.bytes(blockInput || 0), + podmanUtil.format.bytes(blockOutput || 0) + ); + } +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/container.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/container.js new file mode 100644 index 000000000..ae3b14412 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/container.js @@ -0,0 +1,167 @@ +'use strict'; + +'require ui'; +'require dom'; + +'require podman.constants as constant'; +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.model.Container as Container'; +'require podman.form.resource as PodmanFormResource'; + +'require view.podman.container-tab.info as ContainerInfoTab'; +'require view.podman.container-tab.stats as ContainerStatsTab'; +'require view.podman.container-tab.processes as ContainerProcessesTab'; +'require view.podman.container-tab.logs as ContainerLogsTab'; + +/** + * Container detail view with tabbed interface + */ +return podmanView.container.extend({ + container: null, + data: null, + listUrl: L.url('admin/podman/containers'), + + async load() { + // Extract container ID from URL path + // URL format: /cgi-bin/luci/admin/podman/container/ + const path = L.location(); + const matches = path.match(/container\/([a-f0-9]+)/i); + + if (!matches || !matches[1]) { + this.redirectToList(); + return; + } + + const containerId = matches[1]; + const container = Container.getSingleton({ Id: containerId }); + + const inspectData = await container.inspect(); + this.data = inspectData; + return Container.getSingleton(inspectData); + }, + + async render(container) { + if (!container) { + this.redirectToList(); + return; + } + + this.container = container; + this.tabs + .addTab('info', _('Info')) + .addTab('resources', _('Resources')) + .addTab('stats', _('Stats')) + .addTab('ps', _('Processes')) + .addTab('logs', _('Logs')) + // .addTab('health', _('Health')) + .addTab('inspect', _('Inspect')) + // .addTab('console', _('Console')) + ; + + requestAnimationFrame(() => { + this.renderInfoTab(); + this.renderResourcesTab(); + this.renderStatsTab(); + this.renderProcessesTab(); + this.renderLogsTab(); + this.renderInspectTab(); + }); + + return this.super('render', []); + }, + + createHeader() { + return this.super('createHeader', [ + this.container.getName(), + this.container.isRunning(), + this.container.isStopped(), + this.container.isPaused(), + ]); + }, + + stopStreams() { + this.getTabInstance('stats')?.onTabInactive(); + this.getTabInstance('ps')?.onTabInactive(); + this.getTabInstance('logs')?.onTabInactive(); + }, + + async renderInfoTab() { + const content = await ContainerInfoTab.render(this.container); + this.renderTab('info', content); + }, + + async renderResourcesTab() { + const resourceForm = await new PodmanFormResource.init().render(this.container); + this.renderTab('resources', resourceForm, _('Configure resource limits for this container.')); + }, + + async renderStatsTab() { + const content = await ContainerStatsTab.render(this.container); + this.renderTab('stats', content); + }, + + async renderProcessesTab() { + const content = await ContainerProcessesTab.render(this.container); + this.renderTab('ps', content); + }, + + async renderLogsTab() { + const content = await ContainerLogsTab.render(this.container); + this.renderTab('logs', content); + }, + + renderInspectTab() { + this.renderTab('inspect', new podmanUI.JsonArea(this.data).render()); + }, + + async handleStart() { + if (this.container.isRunning()) { + return; + } + + this.loading(_('Start container')); + + if (this.container.isPaused()) { + this.container.unpause().then(() => window.location.reload()); + return; + } + + this.container.start().then(() => window.location.reload()); + }, + + async handleStop() { + if (this.container.isStopped()) { + return; + } + + this.loading(_('Stop container')); + this.stopStreams(); + this.container.stop().then(() => window.location.reload()); + }, + + async handleRestart() { + this.loading(_('Restart container')); + this.stopStreams(); + this.container.restart().then(() => window.location.reload()); + }, + + async handlePause() { + if (!this.container.isRunning()) { + return; + } + + this.loading(_('Pause container')); + this.stopStreams(); + this.container.pause().then(() => window.location.reload()); + }, + + async handleRemove() { + this.confirm([ + E('p', {}, _('Are you sure to remove container?')), + ], async () => { + this.loading(_('Remove container')); + this.container.remove().then(() => this.redirectToList()); + }); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/containers.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/containers.js new file mode 100644 index 000000000..1c2afd188 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/containers.js @@ -0,0 +1,151 @@ +'use strict'; + +'require dom'; +'require ui'; + +'require podman.constants as constant'; +'require podman.rpc as podmanRPC'; +'require podman.utils as utils'; +'require podman.ui as podmanUI'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; +'require podman.form.container as PodmanFormContainer'; + +/** + * Manage podman containers + */ +return podmanView.list.extend({ + sectionName: 'containers', + + title: _('Containers'), + titleSingle: _('Container'), + + async load() { + return podmanRPC.containers.list('all=true'); + }, + + /** + * Update list section + */ + async updateSection() { + this.section.selectKey = 'Id'; + this.section.createForm = PodmanFormContainer; + this.section.handleRefreshCb = () => this.checkInitScripts(); + this.section.toolbarExtraButtons = [ + new podmanUI.Button(constant.ICON.START, ui.createHandlerFn(this, 'handleStart')).render(), + new podmanUI.Button(constant.ICON.STOP, ui.createHandlerFn(this, 'handleStop')).render(), + new podmanUI.Button(constant.ICON.RESTART, ui.createHandlerFn(this, 'handleRestart')).render(), + new podmanUI.Button(constant.ICON.PAUSE, ui.createHandlerFn(this, 'handlePause')).render(), + ]; + + let o; + + o = this.section.option(podmanForm.field.DummyValue, 'Names', _('Name')); + + o = this.section.option(podmanForm.field.DummyValue, 'Id', _('ID')); + o.cfgdatavalue = (container) => container.getDetailLink(utils.truncate(container.getID(), 10)); + o.width = '12%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Image', _('Image')); + o.cfgdatavalue = (container) => container.getImageName(); + o.width = '25%'; + + o = this.section.option(podmanForm.field.DummyValue, 'ImageTag', _('Tag')); + o.cfgdatavalue = (container) => container.getImageTag(); + o.cfgformatter = (imageTag) => utils.truncate(imageTag, 10); + o.cfgtt = (imageTag) => imageTag.length > 10 ? imageTag : ''; + o.width = '11%'; + + o = this.section.option(podmanForm.field.DummyValue, 'State', _('Status')); + o.cfgdatavalue = (container) => container.getStateBadge(); + o.width = '8%'; + + o = this.section.option(podmanForm.field.TimestampDummyValue, 'StartedAt', _('Started At')); + o.width = '17%'; + + o = this.section.option(podmanForm.field.DummyValue, 'InitScript', _('Boot')); + o.cfgvalue = () => E('span', { class: 'autostart-status autostart-' }, '...'); + o.width = '6%'; + }, + + transformMap(map) { + this.checkInitScripts(map); + return map; + }, + + async checkInitScripts(renderedTable) { + const data = this.map.data.data; + + for (const [key, container] of Object.entries(data)) { + const iconEl = (renderedTable || document) + .querySelector(`tr[data-sid="${key}"] td[data-name="InitScript"] .autostart-status`); + + container.checkInitScript().then((initScriptStatus) => { + let initScriptIcon = container.getAutoStartStatusIcon(initScriptStatus); + + if (initScriptStatus === 'missing') { + initScriptIcon = new podmanUI.Tooltip( + initScriptIcon, + _('Start script missing\n\nClick to generate'), + { + class: 'border-0', + click: ui.createHandlerFn(this, 'handleGenerateInitScript') + } + ).render(); + } else if (initScriptStatus === 'disabled') { + initScriptIcon = new podmanUI.Tooltip(initScriptIcon, _('Start script disabled'), { class: 'border-0' }).render(); + } else if (initScriptStatus === 'enabled') { + initScriptIcon = new podmanUI.Tooltip(initScriptIcon, _('Start on boot'), { class: 'border-0' }).render(); + } + + if (iconEl) iconEl.replaceWith(initScriptIcon); + }).catch(() => {}); + } + }, + + async handleGenerateInitScript(event) { + const clickedIcon = event.target; + const recordRow = dom.parent(clickedIcon, 'tr'); + const container = this.map.data.data[recordRow.getAttribute('data-sid')]; + + await container.generateInitScript(); + await container.enableInitScript(); + + this.section.handleRefresh(); + }, + + handleStart() { + return this.updateContainersStatus( + (container) => container.isPaused() ? container.unpause() : container.start(), + _('Start container') + ); + }, + + handleStop() { + return this.updateContainersStatus((container) => container.stop(), _('Stop container')); + }, + + handleRestart() { + return this.updateContainersStatus((container) => container.restart(), _('Restart container')); + }, + + handlePause() { + return this.updateContainersStatus((container) => container.pause(), _('Pause container')); + }, + + async updateContainersStatus(statusFunction, textLoad) { + const section = this.getSection(); + const selected = section.getSelectedData(); + if (selected.length === 0) { + return section.showNoneSelectedWarning(); + } + + for (const [i, container] of selected.entries()) { + this.loading(_('%s: %d/%d').format(textLoad, i + 1, selected.length)); + await statusFunction(container); + } + + ui.hideModal(); + section.handleRefresh(); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/images.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/images.js new file mode 100644 index 000000000..e3cee49ed --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/images.js @@ -0,0 +1,118 @@ +'use strict'; + +'require form'; +'require ui'; + +'require podman.rpc as podmanRPC'; +'require podman.utils as utils'; +'require podman.ui as podmanUI'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; +'require podman.form.image as PodmanFormImage'; + +const IMAGE_ID_TRUNCATE_LEN = 12; +const IMAGE_TAG_TRUNCATE_LEN = 14; + +/** + * Manage podman images + */ +return podmanView.list.extend({ + sectionName: 'images', + + title: _('Images'), + titleSingle: _('Image'), + + async load() { + return podmanRPC.images.list(); + }, + + /** + * Update list section + */ + async updateSection() { + this.section.selectKey = 'Id'; + this.section.handleCreateText = _('Pull Image'); + this.section.getCreateButton = function() { + return new podmanUI.ButtonNew(_('Pull Image'), { + click: ui.createHandlerFn(this, 'handleCreate'), + type: 'add', + }).render(); + }; + this.section.modalCreateTitle = _('Pull Image'); + this.section.createForm = PodmanFormImage; + this.section.toolbarExtraButtons = [ + new podmanUI.Button(_('Update'), ui.createHandlerFn(this, 'handlePullImages')).render(), + ]; + + let o; + + o = this.section.option(podmanForm.field.DummyValue, 'Repository', _('Repository')); + o.cfgdatavalue = async (image) => image.getRepository(); + + o = this.section.option(podmanForm.field.DummyValue, 'Tag', _('Tag')); + o.cfgdatavalue = async (image) => { + const tagValue = image.getTag(); + + if (tagValue.length > IMAGE_TAG_TRUNCATE_LEN) { + return new podmanUI.Tooltip(utils.truncate(tagValue, IMAGE_TAG_TRUNCATE_LEN), tagValue).render(); + } + + return tagValue; + }; + o.width = '17%'; + + o = this.section.option(podmanForm.field.LinkDummyValue, 'ImageId', _('Image ID')); + o.click = (_value, image) => this.section.handleInspect(image); + o.cfgdatavalue = (image) => utils.truncate(image.getID(), IMAGE_ID_TRUNCATE_LEN); + o.width = '15%'; + + o = this.section.option(podmanForm.field.ByteDummyValue, 'Size', _('Size')); + o.width = '12%'; + + o = this.section.option(podmanForm.field.TimestampDummyValue, 'Created', _('Created')); + o.width = '18%'; + + return this.map.render(); + }, + + handlePullImages: async function () { + const selected = this.section.getSelectedData(); + + if (selected.length === 0) { + return this.section.showNoneSelectedWarning(); + } + + const progressEl = E('pre', { class: 'terminal-area' }); + const modal = new podmanUI.Modal(_('Updating Images'), [ progressEl ]); + + /** @type {HTMLElement} */ + const closeButton = modal.getCloseButton(); + closeButton.classList.add('d-none'); + + modal.getButtons = () => [ closeButton ]; + modal.render(); + + const append = (text) => { + progressEl.textContent += text; + progressEl.scrollTop = progressEl.scrollHeight; + }; + + let errors = 0; + let first = true; + for (const image of selected) { + if (!first) append('\n'); + first = false; + try { + await image.streamPull((text) => append(text)); + } catch (err) { + podmanUI.alert(`${_('Error')}: ${err.message}`); + errors++; + } + } + + this.section.handleRefresh(); + closeButton.classList.remove('d-none'); + if (errors === 0) + podmanUI.alert(_('All images updated successfully'), 'success', true); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/modal/autoUpdate.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/modal/autoUpdate.js new file mode 100644 index 000000000..fa2e6b79c --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/modal/autoUpdate.js @@ -0,0 +1,216 @@ +'use strict'; + +'require ui'; + +'require podman.rpc as podmanRPC'; +'require podman.ui as podmanUI'; +'require podman.model.Container as Container'; + +return podmanUI.Modal.extend({ + title: _('Auto Update'), + containers: [], + buttons: [], + + getContent() { + if (!this.containers || this.containers.length === 0) { + this.buttons = [ this.getCloseButton() ]; + return [ + E('p', {}, _('No containers with auto-update label found.')), + E('p', { class: 'mt-sm' }, + _('To enable auto-update for a container, add the label: io.containers.autoupdate=registry') + ), + ]; + } + + this.buttons = [ + this.getCloseButton(), + new podmanUI.ButtonNew(_('Check'), { + click: ui.createHandlerFn(this, 'handleCheckUpdate'), + type: 'positive', + }).render() + ]; + + return [ + E('p', {}, _('Check %d containers for updates?').format(this.containers.length)) + ]; + }, + + setContainers(containers) { + this.containers = containers ?? []; + return this; + }, + + getButtons() { + return this.buttons; + }, + + getCloseButton() { + return new podmanUI.ButtonNew(_('Close'), { + click: () => ui.hideModal(), + type: 'remove', + }).render(); + }, + + async handleCheckUpdate() { + const containersWithUpdates = []; + const checkErrors = []; + + for (const [index, container] of this.containers.entries()) { + podmanUI.showSpinningModal(null, _('Check for updates: %s/%s').format(index + 1, this.containers.length)); + + const updateState = await container.checkImageUpdate(); + if (updateState.hasUpdate) { + containersWithUpdates.push(container); + } else if (updateState.error) { + checkErrors.push({ name: container.getName(), error: updateState.error }); + } + } + + if (containersWithUpdates.length === 0) { + const content = [ E('p', {}, _('No new updates available.')) ]; + if (checkErrors.length > 0) { + content.push(E('p', { class: 'mt-sm' }, _('%d check(s) failed:').format(checkErrors.length))); + checkErrors.forEach((e) => content.push(E('p', { class: 'text-error' }, `${e.name}: ${e.error}`))); + } + const modal = new podmanUI.Modal(_('Check Complete'), content); + modal.getButtons = () => [ this.getCloseButton() ]; + modal.render(); + return; + } + + const updateContent = [ + E('p', {}, _('Select containers to update')), + ]; + + const checkboxes = {}; + containersWithUpdates.forEach((container) => { + const skipReason = container.getUpdateSkipReason(); + const checkbox = new ui.Checkbox(0); + const checkboxNode = checkbox.render(); + const checkboxInputNode = checkboxNode.querySelector('input[type="checkbox"]'); + const checkboxId = checkboxInputNode.id; + + if (skipReason) { + checkboxInputNode.disabled = true; + } else { + checkboxes[container.getID()] = { container, checkbox }; + } + + const inner = skipReason + ? new podmanUI.Tooltip(checkboxNode, skipReason).render() + : checkboxNode; + + updateContent.push(E('div', { class: 'd-flex align-center checkbox-with-label mb-xs' }, [ + inner, + E('label', { for: checkboxId }, container.getName()), + ])); + }); + + const modal = new podmanUI.Modal(_('Apply Updates'), updateContent); + modal.getButtons = () => [ + this.getCloseButton(), + new podmanUI.ButtonNew(_('Update'), { + click: async () => { + const selectedUpdates = Object.values(checkboxes) + .filter(({ checkbox }) => checkbox.isChecked()) + .map(({ container }) => container); + + if (selectedUpdates.length === 0) { + return; + } + + await this.handleUpdate(selectedUpdates); + }, + type: 'positive', + }).render() + ]; + modal.render(); + }, + + async handleUpdate(containers) { + const failures = []; + const oldImages = []; + + const counterEl = E('strong', {}, ''); + const spinnerEl = E('div', { class: 'spinning' }); + const headerEl = E('div', { class: 'd-flex align-center mb-sm gap-5' }, [ + counterEl, + spinnerEl, + ]); + const log = new podmanUI.StreamLog(); + + const finalize = () => { + ui.hideModal(); + if (failures.length === 0 && imageWarnings.length === 0) { + podmanUI.alert(_('Containers updated successfully'), 'success', true); + return; + } + const lines = []; + if (failures.length > 0) { + const succeeded = containers.length - failures.length; + lines.push(E('p', {}, _('%d updated, %d failed:').format(succeeded, failures.length))); + failures.forEach(f => lines.push(E('p', {}, `${f.name}: ${f.error}`))); + } else { + lines.push(E('p', {}, _('Containers updated successfully.'))); + } + imageWarnings.forEach(w => lines.push(E('p', { class: 'mt-sm' }, w))); + podmanUI.alert(lines, failures.length > 0 ? 'warning' : 'info'); + }; + + const closeBtn = new podmanUI.ButtonNew(_('Close'), { + click: () => finalize(), + type: 'negative', + }).render(); + closeBtn.disabled = true; + + const modal = new podmanUI.Modal(_('Apply Updates'), [ headerEl, log.render() ]); + modal.getButtons = () => [ closeBtn ]; + modal.render(); + + let imageWarnings = []; + + for (const [i, container] of containers.entries()) { + counterEl.textContent = _('Update image: %d/%d - %s').format(i + 1, containers.length, container.getName()); + log.append('━━ ' + container.getName() + ' ━━\n'); + try { + const inspected = Container.getSingleton(await container.inspect()); + const result = await inspected.updateImage((line) => log.append(line)); + if (result?.oldImage) oldImages.push({ image: result.oldImage }); + } catch (err) { + const msg = err.message || String(err); + log.append('✗ ' + msg + '\n'); + failures.push({ name: container.getName(), error: msg }); + } + log.append('\n'); + } + + if (oldImages.length > 0) { + counterEl.textContent = _('Cleaning up old images...'); + log.append('━━ ' + _('Cleaning up old images...') + ' ━━\n'); + + const allContainers = await podmanRPC.containers.list('all=true').catch(() => []); + const normalize = id => (id || '').replace(/^sha256:/, '').toLowerCase(); + const usedIds = new Set(allContainers.map(c => normalize(c.ImageID))); + + const seen = new Set(); + for (const { image } of oldImages) { + const id = normalize(image.getID()); + if (seen.has(id)) continue; + seen.add(id); + + if (usedIds.has(id)) { + const warning = _('Old image still used by other containers'); + imageWarnings.push(warning); + log.append('! ' + warning + '\n'); + } else { + await image.remove(); + log.append('✓ ' + _('Removed old image %s').format(image.getID().substring(0, 12)) + '\n'); + } + } + } + + counterEl.textContent = _('Done.'); + spinnerEl.classList.remove('spinning'); + closeBtn.disabled = false; + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/modal/prune.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/modal/prune.js new file mode 100644 index 000000000..8a1cef4c3 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/modal/prune.js @@ -0,0 +1,120 @@ +'use strict'; + +'require ui'; + +'require podman.rpc as podmanRPC'; +'require podman.utils as podmanUtil'; +'require podman.ui as podmanUI'; + +return podmanUI.Modal.extend({ + title: _('Cleanup Unused Resources'), + + getContent() { + this.checkboxPruneAll = new ui.Checkbox(1, { + hiddenname: 'prune-all-images' + }); + this.checkboxPruneVolumes = new ui.Checkbox(0, { + hiddenname: 'prune-volumes' + }); + + const pruneAllNode = this.checkboxPruneAll.render(); + const pruneVolumesNode = this.checkboxPruneVolumes.render(); + const pruneAllId = pruneAllNode.querySelector('input[type="checkbox"]').id; + const pruneVolumesId = pruneVolumesNode.querySelector('input[type="checkbox"]').id; + + return E('div', {}, [ + E('p', {}, _('Select what to clean up:')), + + E('div', { + class: 'd-flex align-center checkbox-with-label', + style: 'margin-bottom: 2px;' + }, [ + pruneAllNode, + E('label', { + for: pruneAllId + }, _('Remove all unused images (not just dangling)')), + ]), + + E('div', { + class: 'd-flex align-center checkbox-with-label' + }, [ + pruneVolumesNode, + E('label', { + for: pruneVolumesId + }, _('Remove unused volumes')), + ]), + ]); + }, + + getButtons() { + return [ + this.getCloseButton(), + new podmanUI.ButtonNew(_('Clean Up Now'), { + click: ui.createHandlerFn(this, 'handlePrune'), + type: 'positive', + }).render() + ]; + }, + + async handlePrune() { + const allImages = this.checkboxPruneAll.getValue() === '1'; + const volumes = this.checkboxPruneVolumes.getValue() === '1'; + podmanUI.showSpinningModal(_('Clean Up Now'), _( + 'Removing unused resources, please wait...')); + + const result = await podmanRPC.system.prune(allImages, volumes); + let freedSpace = 0; + const deletedItems = []; + + const reportTypes = [{ + key: 'ContainerPruneReports', + label: _('Containers') + }, + { + key: 'ImagePruneReports', + label: _('Images') + }, + { + key: 'VolumePruneReports', + label: _('Volumes') + } + ]; + + reportTypes.forEach((type) => { + const reports = result[type.key]; + if (reports && reports.length > 0) { + reports.forEach((r) => { + if (r.Size > 0) freedSpace += r.Size; + }); + deletedItems.push('%d %s'.format(reports.length, type.label + .toLowerCase())); + } + }); + + const modal = new podmanUI.Modal(_('Cleanup Complete')); + modal.handleClose = null; + modal.handleSubmit = () => { + ui.hideModal(); + window.location.reload(); + }; + modal.content = [ + E('p', {}, _('Cleanup successful!')), + ]; + + if (deletedItems.length > 0) { + modal.content.push(E('p', { + class: 'mt-sm' + }, _('Removed: %s').format(deletedItems.join(', ')))); + } else { + modal.content.push(E('p', { + class: 'mt-sm' + }, _('No unused resources found'))); + } + + modal.content.push(E('p', { + class: 'mt-sm text-success text-bold' + }, _('Space freed: %s').format(podmanUtil.format.bytes(freedSpace)))); + + modal.render(); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/networks.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/networks.js new file mode 100644 index 000000000..1bc2cf5d9 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/networks.js @@ -0,0 +1,128 @@ +'use strict'; + +'require network'; +'require ui'; + +'require podman.rpc as podmanRPC'; +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.form as podmanForm'; +'require podman.form.network as PodmanFormNetwork'; + +/** + * Manage podman networks + */ +return podmanView.list.extend({ + sectionName: 'networks', + + title: _('Networks'), + titleSingle: _('Network'), + + async load() { + return podmanRPC.networks.list(); + }, + + /** + * Update list section + */ + async updateSection() { + this.section.selectKey = 'ID'; + this.section.createForm = PodmanFormNetwork; + this.section.handleRemove = async () => this.removeNetworks(); + + let o; + + o = this.section.option(podmanForm.field.LinkDummyValue, 'name', _('Name')); + o.click = (_value, net) => this.section.handleInspect(net); + + o = this.section.option(podmanForm.field.DummyValue, 'Int', _('Int')); + o.cfgdatavalue = (net) => this.getIntegrationIcon(net); + o.width = '5%'; + + o = this.section.option(podmanForm.field.DummyValue, 'driver', _('Driver')); + o.width = '10%'; + + const ipPairCell = (getV4, getV6) => (net) => E('div', {}, [ + E('span', {}, getV4(net) || '-'), + E('br'), + E('span', {}, getV6(net) || '-'), + ]); + + o = this.section.option(podmanForm.field.DummyValue, 'Subnet', _('Subnet')); + o.cfgdatavalue = ipPairCell(n => n.getSubnetIP4(), n => n.getSubnetIP6()); + o.width = '20%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Gateway', _('Gateway')); + o.cfgdatavalue = ipPairCell(n => n.getGatewayIP4(), n => n.getGatewayIP6()); + o.width = '20%'; + + o = this.section.option(podmanForm.field.DateDummyValue, 'created', _('Created')); + o.width = '20%'; + + await network.flushCache(); + }, + + async removeNetworks() { + const selected = this.section.getSelectedData(); + if (selected.length === 0) { + return this.section.showNoneSelectedWarning(); + } + + const checkboxRemoveIntegration = new ui.Checkbox('1', { hiddenname: 'remove-integration' }); + const checkboxNode = checkboxRemoveIntegration.render(); + const checkboxId = checkboxNode.querySelector('input[type="checkbox"]').id; + + this.confirm([ + E('p', {}, _('Are you sure to delete the records?')), + E('div', { class: 'd-flex align-center checkbox-with-label' }, [ + checkboxNode, + E('label', { for: checkboxId }, _('Remove OpenWrt integration')), + ]), + ], async () => { + for (const [i, item] of selected.entries()) { + this.loading(_('Deleting records: %s/%s').format(i + 1, selected.length)); + + await item.remove(); + if (checkboxRemoveIntegration.getValue() === '1') { + await item.integrationRemove(); + } + } + + ui.hideModal(); + this.section.handleRefresh(); + }); + }, + + async getIntegrationIcon(net) { + const { hasInterface, hasDevice, hasDnsmasqExclusion } = await net.integrationCheck(); + + if (hasInterface && hasDevice && hasDnsmasqExclusion) { + return new podmanUI.Tooltip('✅', _('OpenWrt integration exists\n\nClick to remove'), { + click: () => { + this.confirm([ + E('p', {}, _('Are you sure to remove integration?')), + ], async () => { + await net.integrationRemove(); + this.section.handleRefresh(); + }); + }, + }).render(); + } + + const tooltip = []; + tooltip.push(_('OpenWrt integration incomplete')); + tooltip.push(''); + tooltip.push(`${_('Interface')}: ${String(hasInterface)}`); + tooltip.push(`${_('Device')}: ${String(hasDevice)}`); + if (await net.hasDnsmasq()) { + tooltip.push(`${_('Dnsmasq')}: ${String(hasDnsmasqExclusion)}`); + } + tooltip.push(''); + tooltip.push(_('Click icon to repair')); + + return new podmanUI.Tooltip('❌', tooltip.join('\n'), { + click: () => + net.integrationRepair().then(() => this.section.handleRefresh()), + }).render(); + } +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/overview.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/overview.js new file mode 100644 index 000000000..6ef3a1258 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/overview.js @@ -0,0 +1,202 @@ +'use strict'; + +'require dom'; +'require ui'; + +'require podman.rpc as podmanRPC'; +'require podman.utils as podmanUtil'; +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; + +'require view.podman.modal.prune as PruneModal'; +'require view.podman.modal.autoUpdate as AutoUpdateModal'; + +/** + * Podman Overview Dashboard View + */ +return podmanView.base.extend({ + async load() { + return Promise.all([ + podmanRPC.system.version(), + podmanRPC.system.info(), + ]) + }, + + async render([version, info]) { + const sections = await Promise.all([ + this.createToolbar(), + this.createInfoSection(version, info), + this.createDebugSection(), + this.createResourcesSection(), + ]); + + this.loadAdditionalInformation(); + + return E('div', {}, sections.filter(Boolean)); + }, + + async loadAdditionalInformation() { + const [containers, images, volumes, networks, pods] = await Promise.all([ + podmanRPC.containers.list('all=true'), + podmanRPC.images.list(), + podmanRPC.volumes.list(), + podmanRPC.networks.list(), + podmanRPC.pods.list() + ]); + + const runningContainers = containers.filter((container) => container.isRunning()).length; + const runningPods = pods.filter((p) => p.Status === 'Running').length; + + if (this.resourceCardsEl) { + dom.content(this.resourceCardsEl, [ + this.createResourceCards(containers, pods, images, networks, volumes, + runningContainers, runningPods) + ]); + } + }, + + createToolbar() { + return E('div', { class: 'overview-actions mb-sm' }, [ + new podmanUI.Button( + _('Check for Updates'), + ui.createHandlerFn(this, 'handleAutoUpdate'), + 'positive' + ).render(), + new podmanUI.Button( + _('Cleanup / Prune'), + ui.createHandlerFn(this, 'handlePrune'), + 'remove' + ).render() + ]); + }, + + createInfoSection(version, info) { + const memTotal = podmanUtil.format.bytes(info.host?.memTotal); + const memFree = podmanUtil.format.bytes(info.host?.memFree); + + const table = new podmanUI.TableList() + .addRow(_('Podman Version'), version.Version || _('Unknown')) + .addRow(_('API Version'), version.ApiVersion || _('Unknown')) + .addRow(_('CPU'), info.host?.cpus?.toString() || _('Unknown')) + .addRow(_('Memory'), memFree + ' / ' + memTotal) + .addRow(_('Socket Path'), info.host?.remoteSocket?.path || '/run/podman/podman.sock') + .addRow(_('Graph Root'), info.store?.graphRoot || _('Unknown')) + .addRow(_('Run Root'), info.store?.runRoot || _('Unknown')) + .addRow(_('Registries'), this.getRegistries(info)) + ; + + return this.createSection(_('Information'), table.render()); + }, + + createDebugSection() { + this.systemDiagnosticsEl = E('div', { class: 'system-diagnostics', style: 'margin-bottom: 18px;' }, [ + new podmanUI.Button( + _('Load'), + ui.createHandlerFn(this, 'handleLoadSystemDiagnostics'), + 'positive' + ).render(), + ]); + + return this.createSection(_('System Diagnostics'), this.systemDiagnosticsEl); + }, + + createResourcesSection() { + this.resourceCardsEl = E('div', { class: 'resources-cards' }, [ + E('div', { class: 'loading-placeholder' }, [ + E('em', { class: 'spinning' }, _('Loading...')), + ]), + ]); + + return this.createSection(_('Resources'), this.resourceCardsEl); + }, + + createSection(headline, content) { + return E('div', {}, [ + E('h3', {}, headline), + content, + ]); + }, + + getRegistries(info) { + if (info.registries && info.registries.search) { + return info.registries.search.join(', '); + } + return 'docker.io, registry.fedoraproject.org, registry.access.redhat.com'; + }, + + createResourceCards(containers, pods, images, networks, volumes, runningContainers, + runningPods) { + + return E('div', { class: 'overview-cards' }, [ + this.createCard('🐳', _('Containers'), 'containers', containers.length, runningContainers, 'admin/podman/containers'), + this.createCard('🔗', _('Pods'), 'pods', pods.length, runningPods, 'admin/podman/pods'), + this.createCard('💿', _('Images'), 'images', images.length, null, 'admin/podman/images'), + this.createCard('🌐', _('Networks'), 'networks', networks.length, null, 'admin/podman/networks'), + this.createCard('💾', _('Volumes'), 'volumes', volumes.length, null, 'admin/podman/volumes'), + ]); + }, + + createCard(icon, title, slug, total, running, url) { + const statsText = running !== null ? running + ' / ' + total : total.toString(); + + return E('a', { + href: L.url(url), + class: 'd-flex flex-column cursor-pointer align-center overview-card-link overview-card-' + slug, + }, [ + E('div', { class: 'card-link-header d-flex align-center w-100' }, [ + E('span', { class: 'card-link-title text-bold' }, title), + icon, + ]), + E('div', { class: 'w-100' }, [ + E('div', { class: 'card-link-headline text-bold' }, statsText), + running !== null + ? E('div', { class: 'card-link-text' }, _('running') + ' / ' + _('total')) + : E('div', { class: 'card-link-text' }, _('total')) + ]) + ]); + }, + + async handleLoadSystemDiagnostics() { + this.loading(_('Load System diagnostics')); + + const debugData = await podmanRPC.system.debug(); + const statusIcons = { ok: '\u2713', warn: '\u26A0', error: '\u2717' }; + const statusColors = { ok: '#2ecc71', warn: '#e67e22', error: '#e74c3c' }; + + const table = new podmanUI.TableList(); + + (debugData.checks || []).forEach((check) => { + const icon = statusIcons[check.status] || '?'; + const color = statusColors[check.status] || '#666'; + const detail = check.detail || ''; + const message = check.message ? ' \u2014 ' + check.message : ''; + + table.addRow( + _(check.label), + E('span', { style: 'color:' + color }, [ + E('strong', {}, icon + ' '), + E('span', { class: 'cli-value' }, detail), + message + ]) + ); + }); + + dom.content(this.systemDiagnosticsEl, [table.render()]); + ui.hideModal(); + }, + + handlePrune() { + PruneModal.render(); + }, + + handleAutoUpdate() { + this.loading(_('Find updateable containers')); + + // @todo Use podman api filter + return podmanRPC.containers.list('all=true').then((containers) => { + AutoUpdateModal + .setContainers(containers.filter((c) => c.getAutoUpdateLabel())) + .render(); + }); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/containers.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/containers.js new file mode 100644 index 000000000..da496fe42 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/containers.js @@ -0,0 +1,87 @@ +'use strict'; + +'require dom'; +'require form'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.form as podmanForm'; +'require podman.model.Container as Container'; +'require podman.utils as podmanUtil'; +'require podman.TableSelectSection as TableSelectSection'; + +return podmanView.tabContent.extend({ + tab: 'ps', + pod: null, + + async render(pod) { + this.pod = pod; + + const containers = []; + + for (const podContainer of this.pod.getContainers()) { + const containerInspect = await Container.getSingleton(podContainer).inspect(); + const container = Container.getSingleton(containerInspect); + containers.push(container); + } + + this.map = new form.JSONMap({ containers }, ''); + + this.section = this.map.section( + TableSelectSection.TableSelectSection, + 'containers', + '', + '' + ); + this.section.view = this; + this.section.anonymous = true; + this.section.footer = true; + + this.section.selectKey = 'Id'; + // this.section.createForm = PodmanFormContainer; + this.section.handleCreate = null; + this.section.handleRemove = null; + this.section.handleRefresh = null; + + // this.section.handleRefreshCb = () => this.checkInitScripts(); + // this.section.handleRefreshCb = null; + // this.section.toolbarExtraButtons = [ + // new podmanUI.Button('►', ui.createHandlerFn(this, 'handleStart')).render(), + // new podmanUI.Button('◼', ui.createHandlerFn(this, 'handleStop')).render(), + // new podmanUI.Button('↻', ui.createHandlerFn(this, 'handleRestart')).render(), + // ]; + + let o; + + o = this.section.option(podmanForm.field.DummyValue, 'Names', _('Name')); + o.cfgdatavalue = (container) => container.getName(); + + o = this.section.option(podmanForm.field.DummyValue, 'Id', _('ID')); + o.cfgdatavalue = (container) => container.getDetailLink(podmanUtil.truncate(container.getID(), 10)); + o.width = '12%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Image', _('Image')); + o.cfgdatavalue = (container) => container.getImageName(); + o.width = '25%'; + + o = this.section.option(podmanForm.field.DummyValue, 'ImageTag', _('Tag')); + o.cfgdatavalue = (container) => container.getImageTag(); + o.cfgformatter = (imageTag) => podmanUtil.truncate(imageTag, 10); + o.cfgtt = (imageTag) => imageTag.length > 10 ? imageTag : ''; + o.width = '11%'; + + o = this.section.option(podmanForm.field.DummyValue, 'State', _('Status')); + o.cfgdatavalue = (container) => container.getStateBadge(); + o.width = '8%'; + + o = this.section.option(podmanForm.field.DummyValue, 'StartedAt', _('Started At')); + o.cfgdatavalue = (container) => container.getStartedAt(true); + o.width = '17%'; + + o = this.section.option(podmanForm.field.DummyValue, 'InitScript', _('Boot')); + o.cfgvalue = () => E('span', { class: 'autostart-status autostart-' }, '...'); + o.width = '6%'; + + return this.map.render(); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/info.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/info.js new file mode 100644 index 000000000..07a2a639e --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/info.js @@ -0,0 +1,135 @@ +'use strict'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.utils as podmanUtil'; +'require podman.model.Container as Container'; + +const DASH = '-'; + +function joinOrDash(values) { + return Array.isArray(values) && values.length > 0 ? values.join(', ') : DASH; +} + +function valueOrDash(value) { + return (value === undefined || value === null || value === '') ? DASH : value; +} + +return podmanView.tabContent.extend({ + tab: 'info', + pod: null, + + render(pod) { + this.pod = pod; + + const sections = [ + this.basicSection(), + this.configSection(), + this.resourcesSection(), + this.networkSection(), + ]; + + const labels = this.labelsSection(); + if (labels) sections.push(labels); + + return sections; + }, + + basicSection() { + const table = new podmanUI.TableList(); + + table + .addRow(_('Name'), this.pod.getName()) + .addRow(_('ID'), (this.pod.getID() || '').substring(0, 64)) + .addRow(_('Status'), this.pod.getStatusBadge()) + .addRow(_('Created'), podmanUtil.format.date(this.pod.getCreatedAt())); + + return E('div', {}, [ + E('h4', {}, _('Basic Information')), + table.render(), + ]); + }, + + configSection() { + const table = new podmanUI.TableList(); + + const infraId = this.pod.getInfraId(); + const infraIdCell = infraId + ? E('a', { href: L.url('admin/podman/container', infraId) }, infraId.substring(0, 12)) + : DASH; + + table + .addRow(_('Hostname'), valueOrDash(this.pod.Hostname)) + .addRow(_('Cgroup parent'), valueOrDash(this.pod.CgroupParent)) + .addRow(_('Exit policy'), valueOrDash(this.pod.ExitPolicy)) + .addRow(_('Restart policy'), valueOrDash(this.pod.RestartPolicy)) + .addRow(_('Shared namespaces'), joinOrDash(this.pod.SharedNamespaces)) + .addRow(_('Infra container'), infraIdCell); + + return E('div', {}, [ + E('h4', {}, _('Configuration')), + table.render(), + ]); + }, + + resourcesSection() { + const table = new podmanUI.TableList(); + + const memLimit = this.pod.memory_limit; + const memLimitDisplay = memLimit > 0 ? podmanUtil.format.bytes(memLimit) : _('Unlimited'); + + const cpuQuota = this.pod.cpu_quota; + const cpuQuotaDisplay = (cpuQuota === undefined || cpuQuota < 0) ? _('Unlimited') : `${cpuQuota} µs`; + + const cpuPeriod = this.pod.cpu_period; + const cpuPeriodDisplay = cpuPeriod > 0 ? `${cpuPeriod} µs` : DASH; + + table + .addRow(_('CPU period'), cpuPeriodDisplay) + .addRow(_('CPU quota'), cpuQuotaDisplay) + .addRow(_('CPU shares'), valueOrDash(this.pod.cpu_shares)) + .addRow(_('CPU set'), valueOrDash(this.pod.cpuset_cpus)) + .addRow(_('Memory limit'), memLimitDisplay); + + return E('div', {}, [ + E('h4', {}, _('Resources')), + table.render(), + ]); + }, + + networkSection() { + const table = new podmanUI.TableList(); + const infra = this.pod.InfraConfig || {}; + + table + .addRow(_('Networks'), joinOrDash(infra.Networks || this.pod.getNetworks())) + .addRow(_('Static IPv4'), valueOrDash(infra.StaticIP)) + .addRow(_('Static MAC'), valueOrDash(infra.StaticMAC)) + .addRow(_('DNS servers'), joinOrDash(infra.DNSServer)) + .addRow(_('DNS search'), joinOrDash(infra.DNSSearch)) + .addRow(_('DNS options'), joinOrDash(infra.DNSOption)) + .addRow(_('Extra hosts'), joinOrDash(infra.HostAdd)) + .addRow(_('Host network'), infra.HostNetwork ? _('Yes') : _('No')); + + return E('div', {}, [ + E('h4', {}, _('Network')), + table.render(), + ]); + }, + + labelsSection() { + const labels = this.pod.getLabels(); + const keys = Object.keys(labels || {}); + if (keys.length === 0) return null; + + const table = new podmanUI.TableList(); + for (const key of keys) { + table.addRow(key, labels[key]); + } + + return E('div', {}, [ + E('h4', {}, _('Labels')), + table.render(), + ]); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/processes.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/processes.js new file mode 100644 index 000000000..d6f577643 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/processes.js @@ -0,0 +1,96 @@ +'use strict'; + +'require dom'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; + +return podmanView.tabContent.extend({ + tab: 'ps', + pod: null, + + render(pod) { + this.pod = pod; + + if (!this.pod.isRunning()) { + return this.warningContent(_('Pod is not running')); + } + + this.tableContent = E('div', { class: 'ps-table-content' }, []); + + return this.renderTabContent('', [ this.tableContent ]); + }, + + onTabActive() { + if (!this.pod || !this.pod.isRunning() || this.processStream) return; + + this.processStream = this.pod.streamTop((ps) => { + this.updateProcessList(ps); + }); + }, + + onTabInactive() { + if (!this.processStream) return; + + this.processStream.stop(); + this.processStream = null; + }, + + updateProcessList(ps) { + if (!ps || !ps.Titles || !ps.Processes || ps.Titles.length === 0) { + return; + } + + const psTable = new podmanUI.Table(); + const columnWidth = `width: ${100 / ps.Titles.length}%;`; + const timeIndex = ps.Titles.indexOf('ELAPSED'); + + ps.Titles.forEach((title) => { + psTable.addHeader(_(title), { style: columnWidth }); + }); + + ps.Processes.forEach((process) => { + psTable.addRow(process.map((detail, i) => ({ + inner: i === timeIndex ? this._formatElapsedTime(detail) : detail + }))); + }); + + dom.content(this.tableContent, psTable.render()); + }, + + // @todo: duplicated code (containers-tab/processes.js) + _formatElapsedTime(timeStr) { + if (!timeStr) return '-'; + + const result = []; + const pattern = /(\d+(?:\.\d+)?)([ydhms])/g; + let match; + + while ((match = pattern.exec(timeStr)) !== null) { + let value = parseFloat(match[1]); + const unit = match[2]; + + if (unit === 's' && value > 1000) { + value = Math.floor(value / 1000000000); + + if (value >= 60) { + const mins = Math.floor(value / 60); + const secs = value % 60; + if (mins > 0) result.push(`${mins}m`); + if (secs > 0) result.push(`${secs}s`); + continue; + } + } else { + value = Math.floor(value); + } + + if (unit === 's' || unit === 'm') { + result.push(String(value).padStart(2, '0') + unit); + } else { + result.push(value + unit); + } + } + + return result.length > 0 ? result.join('') : timeStr; + } +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/stats.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/stats.js new file mode 100644 index 000000000..8f832aa70 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod-tab/stats.js @@ -0,0 +1,83 @@ +'use strict'; + +'require dom'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.utils as podmanUtil'; + +return podmanView.tabContent.extend({ + tab: 'stats', + pod: null, + + render(pod) { + this.pod = pod; + + if (!this.pod.isRunning()) { + return this.warningContent(_('Pod is not running')); + } + + this.containerNames = this.buildContainerNameMap(); + this.tableContent = E('div', { class: 'stats-table-content' }, []); + + return this.renderTabContent('', [ this.tableContent ]); + }, + + onTabActive() { + if (!this.pod || !this.pod.isRunning() || this.statsStream) return; + + this.statsStream = this.pod.streamStats((reports) => { + this.updateStats(reports); + }); + }, + + onTabInactive() { + if (!this.statsStream) return; + + this.statsStream.stop(); + this.statsStream = null; + }, + + buildContainerNameMap() { + const map = {}; + for (const c of (this.pod.getContainers() || [])) { + if (c?.Id) map[c.Id] = c.Name; + } + return map; + }, + + updateStats(reports) { + if (!Array.isArray(reports)) return; + + const table = new podmanUI.Table(); + const headers = [ + _('Container'), + _('CID'), + _('CPU %'), + _('Memory'), + _('Memory %'), + _('Net I/O'), + _('Block I/O'), + _('PIDs'), + ]; + const columnWidth = `width: ${100 / headers.length}%;`; + headers.forEach((h) => table.addHeader(h, { style: columnWidth })); + + for (const r of reports) { + const cid = r.CID || ''; + const name = this.containerNames[cid] || r.Name || '-'; + table.addRow([ + { inner: name }, + { inner: podmanUtil.truncate(cid, 12) }, + { inner: r.CPU || '-' }, + { inner: r.MemUsage || '-' }, + { inner: r.Mem || '-' }, + { inner: r.NetIO || '-' }, + { inner: r.BlockIO || '-' }, + { inner: r.PIDS || '-' }, + ]); + } + + dom.content(this.tableContent, table.render()); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/pod.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod.js new file mode 100644 index 000000000..15a791c60 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/pod.js @@ -0,0 +1,150 @@ +'use strict'; + +'require ui'; +'require dom'; + +'require podman.ui as podmanUI'; +'require podman.view as podmanView'; +'require podman.model.Pod as Pod'; + +'require view.podman.pod-tab.info as PodInfoTab'; +'require view.podman.pod-tab.containers as PodContainersTab'; +'require view.podman.pod-tab.stats as PodStatsTab'; +'require view.podman.pod-tab.processes as PodProcessesTab'; + +return podmanView.container.extend({ + pod: null, + data: null, + listUrl: L.url('admin/podman/pods'), + + async load() { + const path = L.location(); + const matches = path.match(/pod\/([a-f0-9]+)/i); + + if (!matches || !matches[1]) { + this.redirectToList(); + return; + } + + const podId = matches[1]; + const pod = Pod.getSingleton({ Id: podId }); + + const inspectData = await pod.inspect(); + this.data = inspectData; + + return Pod.getSingleton(inspectData); + }, + + async render(pod) { + if (!pod) { + this.redirectToList(); + return; + } + + this.pod = pod; + this.tabs + .addTab('info', _('Info')) + .addTab('containers', _('Containers')) + .addTab('stats', _('Stats')) + .addTab('ps', _('Processes')) + .addTab('inspect', _('Inspect')); + + requestAnimationFrame(() => { + this.renderInfoTab(); + this.renderContainersTab(); + this.renderStatsTab(); + this.renderProcessesTab(); + this.renderInspectTab(); + }); + + return this.super('render', []); + }, + + createHeader() { + return this.super('createHeader', [ + this.pod.getName(), + this.pod.isRunning(), + this.pod.isStopped(), + this.pod.isPaused(), + ]); + }, + + stopStreams() { + this.getTabInstance('stats')?.onTabInactive(); + this.getTabInstance('ps')?.onTabInactive(); + }, + + async renderInfoTab() { + const content = await PodInfoTab.render(this.pod); + this.renderTab('info', content); + }, + + async renderContainersTab() { + const content = await PodContainersTab.render(this.pod); + this.renderTab('containers', content); + }, + + async renderStatsTab() { + const content = await PodStatsTab.render(this.pod); + this.renderTab('stats', content); + }, + + async renderProcessesTab() { + const content = await PodProcessesTab.render(this.pod); + this.renderTab('ps', content); + }, + + renderInspectTab() { + this.renderTab('inspect', new podmanUI.JsonArea(this.data).render()); + }, + + async handleStart() { + if (this.pod.isRunning()) { + return; + } + + this.loading(_('Start pod')); + + if (this.pod.isPaused()) { + this.pod.unpause().then(() => window.location.reload()); + return; + } + + this.pod.start().then(() => window.location.reload()); + }, + + async handleStop() { + if (!this.pod.isRunning() && !this.pod.isPaused()) { + return; + } + + this.loading(_('Stop pod')); + this.stopStreams(); + this.pod.stop().then(() => window.location.reload()); + }, + + async handleRestart() { + this.loading(_('Restart pod')); + this.stopStreams(); + this.pod.restart().then(() => window.location.reload()); + }, + + async handlePause() { + if (!this.pod.isRunning()) { + return; + } + + this.loading(_('Pause pod')); + this.stopStreams(); + this.pod.pause().then(() => window.location.reload()); + }, + + async handleRemove() { + this.confirm([ + E('p', {}, _('Are you sure to remove pod?')), + ], async () => { + this.loading(_('Remove pod')); + this.pod.remove().then(() => this.redirectToList()); + }); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/podman.css b/luci-app-podman/htdocs/luci-static/resources/view/podman/podman.css new file mode 100644 index 000000000..55088c1ce --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/podman.css @@ -0,0 +1,431 @@ +:root { + --podman-terminal-bg: #1a1a1a; + --podman-terminal-fg: #0f0; + --warn-background-color-dark: rgba(166, 141, 69, 0.2); +} + +.overview-cards { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: 1.3125rem; +} + +div.cbi-tooltip-container { + width: fit-content; +} + +div.cbi-tooltip-container div.cbi-tooltip { + font-size: 13px; + font-weight: 400; + line-height: normal; +} + +.overview-card-link, +.overview-card-link:hover, +.overview-card-link:focus { + text-decoration: none; + color: inherit; + justify-content: space-between; + min-height: 120px; + padding: 1.3125rem; + margin: 0 0 .625rem 0; + transition: all 0.2s ease-in-out; +} + +.overview-card-link .card-link-header { + justify-content: space-between; + margin-bottom: .625rem; + font-size: 20px; +} + +.overview-card-link .card-link-title { + font-size: 16px; + opacity: 0.8; +} + +.overview-card-link .card-link-icon { + font-size: 24px; + opacity: 0.6; +} + +.overview-card-link .card-link-headline { + font-size: 32px; +} + +.overview-card-link .card-link-text { + font-size: 12px; + opacity: 0.7; + margin-top: .3125rem; +} + +.overview-card-containers { + border-left: 4px solid #3498db; +} + +.overview-card-containers .card-link-headline { + color: #3498db; +} + +.overview-card-pods { + border-left: 4px solid #2ecc71; +} + +.overview-card-pods .card-link-headline { + color: #2ecc71; +} + +.overview-card-images { + border-left: 4px solid #9b59b6; +} + +.overview-card-images .card-link-headline { + color: #9b59b6; +} + +.overview-card-networks { + border-left: 4px solid #e67e22; +} + +.overview-card-networks .card-link-headline { + color: #e67e22; +} + +.overview-card-volumes { + border-left: 4px solid #34495e; +} + +.overview-card-volumes .card-link-headline { + color: #34495e; +} + +.border-0 { + border: 0 !important; +} + +.cursor-pointer { + cursor: pointer; +} + +.d-flex { + display: flex; +} + +.align-center { + align-items: center; +} + +.align-start { + align-items: start; +} + +.justify-center { + justify-content: center; +} + +.justify-end { + justify-content: end; +} + +.flex-column { + flex-direction: column; +} + +.gap-5 { + gap: .3125rem; +} + +.cbi-button-active { + background: linear-gradient(lightgray, lightgray 25%, var(--background-color-low)); +} + +.badge { + font-size: 0.85em; + padding: 0 4px; + text-align: center; + border-radius: .3125rem; + width: fit-content; +} + +.badge.running { + background-color: var(--success-color-high); +} + +.badge.exited { + background-color: var(--error-color-high); +} + +.badge.created { + background-color: var(--background-color-low); +} + +.badge.paused { + background-color: var(--warning-color-high, #d68a00); +} + +.badge.degraded { + background-color: var(--warning-color-high, #d68a00); +} + +.badge.stopped, +.badge.dead { + background-color: var(--error-color-high); +} + +.badge.running, +.badge.error, +.badge.exited, +.badge.paused, +.badge.degraded, +.badge.stopped, +.badge.dead { + color: white; +} + +.tooltip { + border-bottom: 1px dotted; +} + +.w-100 { + width: 100%; +} + +.d-none { + display: none; +} + +.modal.loading-modal { + padding: 1em; +} + +.cbi-checkbox + label { + margin-left: 3px; +} + +.text-success { + color: var(--success-color-high); +} + +.text-error { + color: var(--error-color-high); +} + +.mr-xs { + margin-right: .3125rem; +} + +.ml-xs { + margin-left: .3125rem; +} + +.ml-sm { + margin-left: .625rem; +} + +.mt-sm { + margin-top: .625rem; +} + +.mb-xs { + margin-bottom: .3125rem; +} + +.mb-sm { + margin-bottom: .625rem; +} + +.p-sm { + padding: .625rem; +} + +.text-bold { + font-weight: bold; +} + +.text-center { + text-align: center !important; +} + +.d-flex { + display: flex; +} + +.gap-xs { + gap: .25rem; +} + +.gap-sm { + gap: .625rem; +} + +.editable-field { + width: 100%; + display: flex; +} + +.table-list>tr>td:first-of-type { + width: 33%; + font-weight: bold; +} + +.code-area, +.json-area, +.terminal-area { + max-height: 600px; + font-weight: 100; + overflow: auto !important; + white-space: pre !important; + display: block; + word-wrap: normal; +} + +.json-area .key { + color: #660066; + font-weight: bold; +} + +.json-area .string { + color: #007700; +} + +.json-area .boolean { + color: #aa5500; +} + +.json-area .null { + color: #777777; +} + +.json-area .number { + color: #0000ff; +} + +.code-area.bash .comment { + color: #888888; + font-style: italic; +} + +.code-area.bash .string { + color: #007700; +} + +.code-area.bash .variable { + color: #aa5500; +} + +.code-area.bash .keyword { + color: #0000ff; + font-weight: bold; +} + +.code-area.bash .number { + color: #ff4500; +} + +.terminal-area { + min-height: 100px; + background: var(--podman-terminal-bg); + color: var(--podman-terminal-fg); +} + +.autostart-enabled { + color: var(--success-color-high); +} + +.autostart-none, +.autostart-disabled { + color: var(--text-color-medium); +} + +.autostart-missing { + color: var(--warn-color-high); + cursor: pointer; +} + +.autostart-error { + color: var(--error-color-high); +} + +.loading-placeholder { + text-align: center; + padding: 30px; +} + +.cbi-button-none { + border: 0; + background: transparent; + box-shadow: none; +} + +.container-toolbar .d-flex { + gap: .25rem; +} + +.checkbox-with-label .cbi-checkbox { + display: flex; + align-items: center; +} + +.container-icon-disconnect { + border: 0; +} + +#log-viewer { + height: 600px; + overflow: auto; + white-space: pre; +} + +#log-lines { + width: 50px; +} + +#log-since, +#log-until { + width: 100px; +} + +#log-lines input, +#log-since input, +#log-until input { + width: inherit; +} + +.cbi-section-table-select { + table-layout: fixed; +} + +th[data-widget="Podman.Form.SelectDummyValue"] { + padding: .625rem .625rem 8px !important; +} + +th[data-widget="Podman.Form.SelectDummyValue"] .cbi-checkbox { + margin-top: 1px; +} + +td[data-widget="Podman.Form.SelectDummyValue"] { + padding: .625rem .625rem 7px !important; +} + +td[data-widget="Podman.Form.SelectDummyValue"] .cbi-checkbox { + margin-top: 2px; +} + +.podman-stream-log { + font-family: monospace; + font-size: 0.85rem; + line-height: 1.35; + background: #1e1e1e; + color: #e8e8e8; + border: 1px solid #444; + border-radius: 3px; + padding: 0.5rem 0.75rem; + max-height: 320px; + overflow-y: auto; + overflow-x: auto; + white-space: pre; +} + +.podman-stream-log .log-line { + white-space: pre; + word-break: keep-all; +} diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/pods.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/pods.js new file mode 100644 index 000000000..83d21960d --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/pods.js @@ -0,0 +1,172 @@ +'use strict'; + +'require ui'; + +'require podman.rpc as podmanRPC'; +'require podman.utils as utils'; +'require podman.ui as podmanUI'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; +'require podman.form.pod as PodmanFormPod'; +'require podman.model.Container as Container'; + +return podmanView.list.extend({ + sectionName: 'pods', + + title: _('Pods'), + titleSingle: _('Pod'), + + async load() { + return podmanRPC.pods.list(); + }, + + async updateSection() { + this.section.selectKey = 'Id'; + this.section.createForm = PodmanFormPod; + this.section.toolbarExtraButtons = [ + new podmanUI.Button('►', ui.createHandlerFn(this, 'handleStart')).render(), + new podmanUI.Button('◼', ui.createHandlerFn(this, 'handleStop')).render(), + new podmanUI.Button('↻', ui.createHandlerFn(this, 'handleRestart')).render(), + new podmanUI.Button('❚❚', ui.createHandlerFn(this, 'handlePause')).render(), + ]; + + let o; + + o = this.section.option(podmanForm.field.DummyValue, 'Name', _('Name')); + + o = this.section.option(podmanForm.field.DummyValue, 'Id', _('ID')); + o.cfgdatavalue = (pod) => pod.getDetailLink(utils.truncate(pod.getID(), 16)); + o.width = '20%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Status', _('Status')); + o.cfgdatavalue = (pod) => pod.getStatusBadge(); + o.width = '10%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Containers', _('Containers')); + o.cfgdatavalue = (pod) => this.renderContainersCell(pod); + o.width = '12%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Networks', _('Networks')); + o.cfgdatavalue = (pod) => { + const nets = pod.getNetworks(); + return nets.length > 0 ? nets.join(', ') : '-'; + }; + o.width = '15%'; + + o = this.section.option(podmanForm.field.DateDummyValue, 'Created', _('Created')); + o.width = '20%'; + }, + + renderContainersCell(pod) { + const count = pod.getContainers().length; + if (count === 0) { + return E('span', {}, '0'); + } + + return E('a', { + href: '#', + class: 'text-bold', + click: (ev) => { + ev.preventDefault(); + this.handleShowContainers(pod); + } + }, `${count} ▸`); + }, + + handleShowContainers(pod) { + const containers = pod.getContainers(); + + const headerRow = E('tr', {}, [ + E('th', {}, _('Name')), + E('th', {}, _('ID')), + E('th', {}, _('Status')), + ]); + + const rows = containers.map((c) => { + const container = Container.getSingleton(c); + const idShort = utils.truncate(container.getID(), 10); + const idCell = container.getDetailLink(idShort); + + return E('tr', {}, [ + E('td', {}, container.getName()), + E('td', {}, idCell), + E('td', {}, container.getStateBadge()), + ]); + }); + + // @todo: create table with helpers or show a simpler list + const table = E('table', { + class: 'table cbi-section-table' + }, [ + E('thead', {}, headerRow), + E('tbody', {}, rows.length > 0 ? rows : [ + E('tr', {}, E('td', { + colspan: 3, + class: 'text-center' + }, _('No containers in this pod'))), + ]), + ]); + + const title = _('Containers in pod %s').format(pod.getName()); + const modal = new podmanUI.Modal(title, [table]); + modal.render(); + }, + + handleStart() { + return this.runOnSelected( + (pod) => pod.isPaused() ? pod.unpause() : pod.start(), + _('Starting pod') + ); + }, + + handleStop() { + return this.runOnSelected((pod) => pod.stop(), _('Stopping pod')); + }, + + handleRestart() { + return this.runOnSelected((pod) => pod.restart(), _('Restarting pod')); + }, + + handlePause() { + return this.runOnSelected((pod) => pod.pause(), _('Pausing pod')); + }, + + async runOnSelected(action, textLoad) { + const section = this.getSection(); + const selected = section.getSelectedData(); + if (selected.length === 0) { + return section.showNoneSelectedWarning(); + } + + const failures = []; + for (const [i, pod] of selected.entries()) { + this.loading(_('%s: %d/%d').format(textLoad, i + 1, selected.length)); + const result = await action(pod); + if (result?.Errs?.length > 0) { + failures.push({ + name: pod.getName(), + errs: result.Errs + }); + } + } + + ui.hideModal(); + + if (failures.length > 0) { + const blocks = failures.map(({ + name, + errs + }) => E('div', { + class: 'mb-sm' + }, [ + E('strong', {}, name + ':'), + E('ul', { + class: 'm-0' + }, errs.map((e) => E('li', {}, e))), + ])); + podmanUI.alert(blocks, 'error'); + } + + section.handleRefresh(); + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/secrets.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/secrets.js new file mode 100644 index 000000000..accec5a5c --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/secrets.js @@ -0,0 +1,59 @@ +'use strict'; + +'require form'; + +'require podman.utils as utils'; +'require podman.rpc as podmanRPC'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; +'require podman.form.secret as PodmanFormSecret'; + +const SECRET_LABELS_TRUNCATE_LEN = 30; + +/** + * Manage podman secrets + */ +return podmanView.list.extend({ + sectionName: 'secrets', + + title: _('Secrets'), + titleSingle: _('Secret'), + + /** + * Load secret data + */ + async load() { + return podmanRPC.secrets.list(); + }, + + /** + * Update list section + */ + async updateSection() { + this.section.selectKey = 'ID'; + this.section.createForm = PodmanFormSecret; + + let o; + + o = this.section.option(podmanForm.field.LinkDummyValue, 'Name', _('Name')); + o.cfgdatavalue = (secret) => secret.getName(); + o.click = (_value, secret) => this.section.handleInspect(secret, ['SecretData']); + + o = this.section.option(podmanForm.field.DummyValue, 'Driver', _('Driver')); + o.cfgdatavalue = (secret) => secret.getDriver(); + o.width = '10%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Labels', _('Labels')); + o.cfgdatavalue = (secret) => { + const labels = secret.getLabels(); + const keys = Object.keys(labels); + return keys.length ? keys.map((key) => `${key}=${labels[key]}`).join(', ') : '-'; + }; + o.cfgformatter = (labels) => utils.truncate(labels, SECRET_LABELS_TRUNCATE_LEN); + o.cfgtt = (labels) => labels.length > SECRET_LABELS_TRUNCATE_LEN ? labels : ''; + o.width = '20%'; + + o = this.section.option(podmanForm.field.DateDummyValue, 'CreatedAt', _('Created')); + o.width = '20%'; + }, +}); diff --git a/luci-app-podman/htdocs/luci-static/resources/view/podman/volumes.js b/luci-app-podman/htdocs/luci-static/resources/view/podman/volumes.js new file mode 100644 index 000000000..1c7b68a93 --- /dev/null +++ b/luci-app-podman/htdocs/luci-static/resources/view/podman/volumes.js @@ -0,0 +1,122 @@ +'use strict'; + +'require ui'; +'require fs'; + +'require podman.utils as utils'; +'require podman.rpc as podmanRPC'; +'require podman.ui as podmanUI'; +'require podman.form as podmanForm'; +'require podman.view as podmanView'; +'require podman.form.volume as PodmanFormVolume'; +'require podman.form.volumeImport as PodmanFormVolumeImport'; + +const VOLUME_MOUNT_TRUNCATE_LEN = 30; + +return podmanView.list.extend({ + sectionName: 'volumes', + + title: _('Volumes'), + titleSingle: _('Volume'), + + async load() { + return podmanRPC.volumes.list(); + }, + + async updateSection() { + this.section.selectKey = 'Name'; + this.section.createForm = PodmanFormVolume; + this.section.toolbarExtraButtons = [ + new podmanUI.ButtonNew(_('Import'), { + click: ui.createHandlerFn(this, 'handleImport'), + }).render(), + new podmanUI.ButtonNew(_('Export'), { + click: ui.createHandlerFn(this, 'handleExport'), + }).render(), + ]; + + let o; + + o = this.section.option(podmanForm.field.LinkDummyValue, 'VolumeName', _('Name')); + o.cfgdatavalue = (volume) => utils.truncate(volume.getName(), 20); + o.cfgtt = (_cfg, volume) => volume.getName().length > 20 ? volume.getName() : ''; + o.click = (_cfg, volume) => this.section.handleInspect(volume); + + o = this.section.option(podmanForm.field.DummyValue, 'Driver', _('Driver')); + o.width = '10%'; + + o = this.section.option(podmanForm.field.DummyValue, 'Mountpoint', _('Mountpoint')); + o.cfgformatter = (mountpoint) => utils.truncate(mountpoint, VOLUME_MOUNT_TRUNCATE_LEN); + o.cfgtt = (mountpoint) => mountpoint.length > VOLUME_MOUNT_TRUNCATE_LEN ? mountpoint : ''; + o.width = '35%'; + + o = this.section.option(podmanForm.field.DateDummyValue, 'CreatedAt', _('Created')); + o.width = '20%'; + }, + + handleImport() { + const fileInput = E('input', { + type: 'file', + accept: '.tar,.tar.gz,.tgz', + class: 'hidden' + }); + + fileInput.addEventListener('change', async (ev) => { + document.body.removeChild(fileInput); + const file = ev.target.files[0]; + if (!file) return; + + const importForm = new PodmanFormVolumeImport.init(); + const formContent = await importForm.render(file); + const title = _('Import from archive: %s').format(file.name); + const modal = new podmanUI.Modal(title, [ formContent ]); + modal.getButtons = () => [ + modal.getCloseButton(), + new podmanUI.ButtonNew(_('Import'), { + click: async () => { + await importForm.handleCreate(); + await this.section.handleRefresh(); + }, + type: 'positive', + }).render(), + ]; + modal.render(); + }); + + document.body.appendChild(fileInput); + fileInput.click(); + }, + + async handleExport() { + const selected = this.section.getSelectedData(); + + if (selected.length === 0) { + return this.section.showNoneSelectedWarning(); + } + + let errors = 0; + for (const [index, volume] of selected.entries()) { + this.loading(_('Exporting selected volumes: %d/%d').format(index + 1, selected.length)); + + const volumeName = volume.getName(); + try { + const blob = await fs.exec_direct('/usr/libexec/podman-api', ['volume_export', volumeName], 'blob'); + + const url = URL.createObjectURL(blob); + const a = E('a', { href: url, download: `${volumeName}.tar` }); + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + setTimeout(() => URL.revokeObjectURL(url), 100); + } catch (err) { + podmanUI.alert(_('Error: %s').format(err.message), 'error'); + errors++; + } + } + + ui.hideModal(); + + if (errors === 0) + this.success(_('All volumes exported successfully')); + }, +}); diff --git a/luci-app-podman/po/de/podman.po b/luci-app-podman/po/de/podman.po new file mode 100644 index 000000000..f0825b810 --- /dev/null +++ b/luci-app-podman/po/de/podman.po @@ -0,0 +1,1090 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-05-03 11:05+0000\n" +"Last-Translator:\n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "%d check(s) failed:" +msgstr "%d Prüfung(en) fehlgeschlagen:" + +msgid "%d updated, %d failed:" +msgstr "%d aktualisiert, %d fehlgeschlagen:" + +msgid "%s created successfully" +msgstr "%s erfolgreich erstellt" + +msgid "%s: %d/%d" +msgstr "%s: %d/%d" + +msgid "(none)" +msgstr "(keine)" + +msgid "-- Select %s --" +msgstr "-- %s auswählen --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 Zeichen: Nur Buchstaben, Zahlen, Unterstriche (_) und Bindestriche (-)" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m, 1g oder -1 für unbegrenzt" + +msgid "API Version" +msgstr "API-Version" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "Container zu einem bestehenden Pod hinzufügen. Netzwerk, Hostname und Port-Mappings stammen vom Infra-Container des Pods." + +msgid "All images updated successfully" +msgstr "Alle Images erfolgreich aktualisiert" + +msgid "All volumes exported successfully" +msgstr "Alle Volumes erfolgreich exportiert" + +msgid "Allocate TTY (-t)" +msgstr "TTY zuweisen (-t)" + +msgid "Allocate container IP from this range" +msgstr "Container-IP aus diesem Bereich zuweisen" + +msgid "Always" +msgstr "Immer" + +msgid "Apply Updates" +msgstr "Updates anwenden" + +msgid "Are you sure to delete the records?" +msgstr "Einträge wirklich löschen?" + +msgid "Are you sure to remove container?" +msgstr "Container wirklich entfernen?" + +msgid "Are you sure to remove integration?" +msgstr "Integration wirklich entfernen?" + +msgid "Auto Remove (--rm)" +msgstr "Automatisch entfernen (--rm)" + +msgid "Auto Update" +msgstr "Automatische Aktualisierung" + +msgid "Auto-Update" +msgstr "Auto-Update" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "Netzwerkschnittstelle, Bridge und Firewall-Zone in OpenWrt automatisch konfigurieren. Dringend empfohlen für korrektes Container-Networking unter OpenWrt." + +msgid "Automatically start the container after it is created" +msgstr "Container nach der Erstellung automatisch starten" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "Container automatisch aktualisieren, wenn ein neueres Image verfügbar ist. Fügt Label hinzu: io.containers.autoupdate=registry" + +msgid "Basic Information" +msgstr "Basisinformationen" + +msgid "Block I/O" +msgstr "Block-I/O" + +msgid "Boot" +msgstr "Systemstart" + +msgid "Bridge Interface Name" +msgstr "Schnittstellenname der Bridge" + +msgid "CPU" +msgstr "CPU" + +msgid "CPU Limit" +msgstr "CPU-Limit" + +msgid "CPU Set" +msgstr "CPU-Set" + +msgid "CPU Shares Weight" +msgstr "CPU-Shares Gewichtung" + +msgid "CPU Usage" +msgstr "CPU-Auslastung" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "CPU-Anteile (relatives Gewicht). 0 = Standard." + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "CPUs, auf denen der Pod laufen darf (z. B. \"0-3\" oder \"0,2\")" + +msgid "Cancel" +msgstr "Abbrechen" + +msgid "Check" +msgstr "Prüfen" + +msgid "Check %d containers for updates?" +msgstr "%d Container auf Updates prüfen?" + +msgid "Check Complete" +msgstr "Prüfung abgeschlossen" + +msgid "Check for Updates" +msgstr "Auf Updates prüfen" + +msgid "Check for updates: %s/%s" +msgstr "Auf Updates prüfen: %s/%s" + +msgid "Choose firewall zone for this network." +msgstr "Firewall-Zone für dieses Netzwerk wählen." + +msgid "Clean Up Now" +msgstr "Jetzt bereinigen" + +msgid "Cleaning up old images..." +msgstr "Alte Images werden bereinigt..." + +msgid "Cleanup / Prune" +msgstr "Bereinigen / Prune" + +msgid "Cleanup Complete" +msgstr "Bereinigung abgeschlossen" + +msgid "Cleanup Unused Resources" +msgstr "Nicht verwendete Ressourcen bereinigen" + +msgid "Cleanup successful!" +msgstr "Bereinigung erfolgreich!" + +msgid "Clear logs" +msgstr "Protokolle leeren" + +msgid "Click icon to repair" +msgstr "Symbol anklicken zum Reparieren" + +msgid "Click to disconnect" +msgstr "Klicken zum Trennen" + +msgid "Click to enable/disable" +msgstr "Klicken zum Aktivieren/Deaktivieren" + +msgid "Click to reveal/hide value" +msgstr "Klicken zum Ein-/Ausblenden des Wertes" + +msgid "Close" +msgstr "Schließen" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "Kommagetrennte Liste zusätzlicher Gruppen-IDs" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "Durch Komma getrennte freizugebende Ports (z.B. 6052, 8080/udp)" + +msgid "Command" +msgstr "Befehl" + +msgid "Command to run (space-separated)" +msgstr "Auszuführender Befehl (leerzeichengetrennt)" + +msgid "Configuration" +msgstr "Konfiguration" + +msgid "Configure resource limits for this container." +msgstr "Ressourcenlimits für diesen Container konfigurieren." + +msgid "Confirm" +msgstr "Bestätigen" + +msgid "Connect" +msgstr "Verbinden" + +msgid "Connect to" +msgstr "Verbinden mit" + +msgid "Connected successfully to %s" +msgstr "Erfolgreich mit %s verbunden" + +msgid "Connecting container to network" +msgstr "Container mit Netzwerk verbinden" + +msgid "Console" +msgstr "Konsole" + +msgid "Container" +msgstr "Container" + +msgid "Container Name" +msgstr "Container-Name" + +msgid "Container image has no tag - cannot update" +msgstr "Container-Image hat kein Tag – Aktualisierung nicht möglich" + +msgid "Container image to use" +msgstr "Zu verwendendes Container-Image" + +msgid "Container is not running" +msgstr "Container läuft nicht" + +msgid "Container updated successfully" +msgstr "Container erfolgreich aktualisiert" + +msgid "Containers" +msgstr "Container" + +msgid "Containers in pod %s" +msgstr "Container im Pod %s" + +msgid "Containers updated successfully" +msgstr "Container erfolgreich aktualisiert" + +msgid "Containers updated successfully." +msgstr "Container erfolgreich aktualisiert." + +msgid "Create" +msgstr "Erstellen" + +msgid "Create %s" +msgstr "%s erstellen" + +msgid "Create Command" +msgstr "Erstellungsbefehl" + +msgid "Create Infra Container" +msgstr "Infra-Container erstellen" + +msgid "Create new zone (will be named: podman_)" +msgstr "Neue Zone erstellen (Name: podman_)" + +msgid "Created" +msgstr "Erstellt" + +msgid "Creating %s" +msgstr "Erstelle %s" + +msgid "Creating auto-start configuration for %s" +msgstr "Erstelle Autostart-Konfiguration für %s" + +msgid "DNS Search Domains" +msgstr "DNS-Suchdomänen" + +msgid "DNS Servers" +msgstr "DNS-Server" + +msgid "DNS search domains for the infra container" +msgstr "DNS-Suchdomänen für den Infra-Container" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "DNS-Server für den Infra-Container (gemeinsam für alle Container im Pod)" + +msgid "Delete" +msgstr "Löschen" + +msgid "Deleting records: %d/%d" +msgstr "Einträge werden gelöscht: %d/%d" + +msgid "Deleting records: %s/%s" +msgstr "Einträge werden gelöscht: %s/%s" + +msgid "Destination" +msgstr "Ziel" + +msgid "Device" +msgstr "Gerät" + +msgid "Disabled" +msgstr "Deaktiviert" + +msgid "Disabling init script" +msgstr "Init-Skript wird deaktiviert" + +msgid "Disconnect container from network" +msgstr "Container vom Netzwerk trennen" + +msgid "Disconnected from %s" +msgstr "Von %s getrennt" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "Treiber" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "Treiberspezifische Optionen (kommagetrennt, z. B. type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "IPv6 aktivieren" + +msgid "Enable IPv6 networking" +msgstr "IPv6-Networking aktivieren" + +msgid "Enabling init script" +msgstr "Init-Skript wird aktiviert" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "Geheimnisdaten eingeben (Passwort, Token, Schlüssel etc.)" + +msgid "Entrypoint" +msgstr "Entrypoint" + +msgid "Environment Variables" +msgstr "Umgebungsvariablen" + +msgid "Ephemeral (--rm flag)" +msgstr "Temporär (--rm Flag)" + +msgid "Error" +msgstr "Fehler" + +msgid "Error: %s" +msgstr "Fehler: %s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "Bestehende physische Schnittstelle als Parent. Erforderlich für macvlan/ipvlan-Netzwerke." + +msgid "Expecting: %s" +msgstr "Erwartet: %s" + +msgid "Export" +msgstr "Exportieren" + +msgid "Exporting selected volumes: %d/%d" +msgstr "Ausgewählte Volumes werden exportiert: %d/%d" + +msgid "Expose Ports" +msgstr "Ports freigeben" + +msgid "Failed to create container" +msgstr "Container konnte nicht erstellt werden" + +msgid "Fetching information..." +msgstr "Informationen werden abgerufen..." + +msgid "Find updateable containers" +msgstr "Aktualisierbare Container suchen" + +msgid "Firewall Zone" +msgstr "Firewall-Zone" + +msgid "Gateway" +msgstr "Gateway" + +msgid "Generate" +msgstr "Generieren" + +msgid "Graph Root" +msgstr "Graph-Root" + +msgid "Health" +msgstr "Gesundheit" + +msgid "Hostname" +msgstr "Hostname" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Hostname des Pods (Standard: Pod-Name)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "IP-Adresse" + +msgid "IP Range (CIDR)" +msgstr "IP-Bereich (CIDR)" + +msgid "IPv4 Gateway" +msgstr "IPv4-Gateway" + +msgid "IPv4 Subnet (CIDR)" +msgstr "IPv4-Subnetz (CIDR)" + +msgid "IPv4 gateway address" +msgstr "IPv4-Gateway-Adresse" + +msgid "IPv4 subnet in CIDR notation" +msgstr "IPv4-Subnetz in CIDR-Notation" + +msgid "Image" +msgstr "Image" + +msgid "Image ID" +msgstr "Image-ID" + +msgid "Image is already up-to-date" +msgstr "Image ist bereits aktuell" + +msgid "Image pulled successfully" +msgstr "Image erfolgreich abgerufen" + +msgid "Images" +msgstr "Images" + +msgid "Import" +msgstr "Importieren" + +msgid "Import from archive: %s" +msgstr "Aus Archiv importieren: %s" + +msgid "Importing volume..." +msgstr "Importiere Volume..." + +msgid "Info" +msgstr "Info" + +msgid "Information" +msgstr "Informationen" + +msgid "Infra Image" +msgstr "Infra-Image" + +msgid "Init Script" +msgstr "Init-Skript" + +msgid "Inspect" +msgstr "Inspizieren" + +msgid "Int" +msgstr "Ganz" + +msgid "Interactive" +msgstr "Interaktiv" + +msgid "Interface" +msgstr "Schnittstelle" + +msgid "Internal Network" +msgstr "Internes Netzwerk" + +msgid "Labels" +msgstr "Labels" + +msgid "Labels in key=value format, one per line" +msgstr "Labels im Format Schlüssel=Wert, eines pro Zeile" + +msgid "Leave empty to auto-generate" +msgstr "Leer lassen für automatische Generierung" + +msgid "Links" +msgstr "Links" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Linux-Namespaces, die von den Containern im Pod gemeinsam genutzt werden. Standard entspricht `podman pod create`." + +msgid "Load" +msgstr "Laden" + +msgid "Load System diagnostics" +msgstr "Systemdiagnose laden" + +msgid "Loading init script" +msgstr "Init-Skript wird geladen" + +msgid "Loading..." +msgstr "Lade..." + +msgid "Logs" +msgstr "Logs" + +msgid "Manage podman %s" +msgstr "Podman %s verwalten" + +msgid "Memory" +msgstr "Arbeitsspeicher" + +msgid "Memory %" +msgstr "Speicher %" + +msgid "Memory + Swap Limit" +msgstr "Limit für Arbeitsspeicher + Swap" + +msgid "Memory Limit" +msgstr "Speicherlimit" + +msgid "Memory Usage" +msgstr "Speicherauslastung" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "Speicherlimit (z. B. 512m, 1g)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "Speicherlimit (z.B. 512m, 1g). -1 = unbegrenzt, 0 = Standard." + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Speicherlimit für den Pod (z. B. 512m, 1g)" + +msgid "Mode" +msgstr "Modus" + +msgid "Mount Options" +msgstr "Mount-Optionen" + +msgid "Mountpoint" +msgstr "Mountpoint" + +msgid "Mounts" +msgstr "Mounts" + +msgid "Must be between 0 and 262144" +msgstr "Muss zwischen 0 und 262144 sein" + +msgid "Name" +msgstr "Name" + +msgid "Name for the network" +msgstr "Name für das Netzwerk" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "Name der Bridge-Schnittstelle (z. B. podman0, mynet0). Leer lassen für: <Netzwerkname>0. Hinweis: Falls der generierte Name mit einer bestehenden Schnittstelle kollidiert, wird OpenWrt ihn automatisch hochzählen." + +msgid "Name updated successfully" +msgstr "Name erfolgreich aktualisiert" + +msgid "Network" +msgstr "Netzwerk" + +msgid "Network I/O" +msgstr "Netzwerk-I/O" + +msgid "Network Mode" +msgstr "Netzwerkmodus" + +msgid "Network Name" +msgstr "Netzwerkname" + +msgid "Network driver" +msgstr "Netzwerktreiber" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Netzwerk-Namespace für den Pod. Container im Pod teilen sich dieses Netzwerk." + +msgid "Networks" +msgstr "Netzwerke" + +msgid "Never" +msgstr "Nie" + +msgid "No" +msgstr "Nein" + +msgid "No %s selected" +msgstr "Keine %s ausgewählt" + +msgid "No containers in this pod" +msgstr "Keine Container in diesem Pod" + +msgid "No containers with auto-update label found." +msgstr "Keine Container mit Auto-Update-Label gefunden." + +msgid "No new updates available." +msgstr "Keine neuen Updates verfügbar." + +msgid "No suitable interfaces found" +msgstr "Keine geeigneten Schnittstellen gefunden" + +msgid "No unused resources found" +msgstr "Keine ungenutzten Ressourcen gefunden" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "Anzahl der CPUs (z. B. 0.5, 1.0, 2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "Anzahl der CPUs (z. B. 0.5, 1.0, 2.0). Leer lassen für unbegrenzt." + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Anzahl der CPUs für den Pod (z. B. 0.5, 1.0, 2.0)" + +msgid "Number of lines" +msgstr "Anzahl der Zeilen" + +msgid "OK" +msgstr "OK" + +msgid "Old image still used by other containers" +msgstr "Altes Image wird noch von anderen Containern verwendet" + +msgid "On Failure" +msgstr "Bei Fehler" + +msgid "One per line, format: host:container[/protocol]" +msgstr "Einer pro Zeile, Format: Host:Container[/Protokoll]" + +msgid "One per line, format: key=value" +msgstr "Einer pro Zeile, Format: Schlüssel=Wert" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "Einer pro Zeile. Format: Quelle:Ziel[:Optionen]. Optionen: ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "OpenWrt-Integration vorhanden\n\nKlicken zum Entfernen" + +msgid "OpenWrt integration incomplete" +msgstr "OpenWrt-Integration unvollständig" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "OpenWrt-Integration erfordert die Angabe eines Subnetzes" + +msgid "Override the image used for the infra container" +msgstr "Image für den Infra-Container überschreiben" + +msgid "PIDs" +msgstr "PIDs" + +msgid "Parent Interface" +msgstr "Parent-Schnittstelle" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "Parent-Schnittstelle ist für macvlan/ipvlan-Netzwerke erforderlich" + +msgid "Part of a pod (\"%s\")" +msgstr "Teil eines Pods (\"%s\")" + +msgid "Pausing pod" +msgstr "Pod wird pausiert" + +msgid "Pod" +msgstr "Pod" + +msgid "Pod Name" +msgstr "Pod-Name" + +msgid "Podman Version" +msgstr "Podman-Version" + +msgid "Pods" +msgstr "Pods" + +msgid "Port Mappings" +msgstr "Port-Mappings" + +msgid "Ports" +msgstr "Ports" + +msgid "Privileged" +msgstr "Privilegiert" + +msgid "Privileged Mode" +msgstr "Privilegierter Modus" + +msgid "Processes" +msgstr "Prozesse" + +msgid "Pull Image" +msgstr "Image abrufen" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "Pull hat keine Image-ID zurückgegeben – Pull möglicherweise fehlgeschlagen" + +msgid "Pulling %s" +msgstr "%s wird abgerufen" + +msgid "Read: %s / Write: %s" +msgstr "Gelesen: %s / Geschrieben: %s" + +msgid "Refreshing %s..." +msgstr "Aktualisiere %s..." + +msgid "Registries" +msgstr "Registries" + +msgid "Registry" +msgstr "Registry" + +msgid "Reload" +msgstr "Neu laden" + +msgid "Remove OpenWrt integration" +msgstr "OpenWrt-Integration entfernen" + +msgid "Remove all unused images (not just dangling)" +msgstr "Alle ungenutzten Images entfernen (nicht nur verwaiste)" + +msgid "Remove container" +msgstr "Container entfernen" + +msgid "Remove unused volumes" +msgstr "Ungenutzte Volumes entfernen" + +msgid "Removed: %s" +msgstr "Entfernt: %s" + +msgid "Removing unused resources, please wait..." +msgstr "Ungenutzte Ressourcen werden entfernt, bitte warten..." + +msgid "Repository" +msgstr "Repository" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "Erforderlich für gemeinsamen Netzwerk-Namespace. Nur deaktivieren, wenn nicht benötigt." + +msgid "Resources" +msgstr "Ressourcen" + +msgid "Restart Policy" +msgstr "Restart-Policy" + +msgid "Restart container" +msgstr "Container neu starten" + +msgid "Restart policy updated successfully" +msgstr "Restart-Policy erfolgreich aktualisiert" + +msgid "Restarting pod" +msgstr "Pod wird neu gestartet" + +msgid "Restrict external access to the network" +msgstr "Externen Zugriff auf das Netzwerk einschränken" + +msgid "Run Root" +msgstr "Run-Root" + +msgid "Saving resource" +msgstr "Ressource wird gespeichert" + +msgid "Secret" +msgstr "Geheimnis (Secret)" + +msgid "Secret Data" +msgstr "Geheimnisdaten" + +msgid "Secret Name" +msgstr "Geheimnis-Name" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "Name des Geheimnisses darf nur Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten" + +msgid "Secrets" +msgstr "Geheimnisse" + +msgid "Security Notice:" +msgstr "Sicherheitshinweis:" + +msgid "Select containers to update" +msgstr "Container zum Aktualisieren auswählen" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "Netzwerk für den neuen Container auswählen. Benutzerdefinierte Netzwerke bieten bessere Isolation und DNS-Auflösung zwischen Containern." + +msgid "Select what to clean up:" +msgstr "Wählen Sie aus, was bereinigt werden soll:" + +msgid "Sensitive data is hidden for security reasons." +msgstr "Sensible Daten sind aus Sicherheitsgründen ausgeblendet." + +msgid "Setup OpenWrt Integration" +msgstr "OpenWrt-Integration einrichten" + +msgid "Shared Namespaces" +msgstr "Gemeinsame Namespaces" + +msgid "Shares network with another container" +msgstr "Teilt Netzwerk mit einem anderen Container" + +msgid "Show" +msgstr "Anzeigen" + +msgid "Since" +msgstr "Seit" + +msgid "Size" +msgstr "Größe" + +msgid "Socket Path" +msgstr "Socket-Pfad" + +msgid "Source" +msgstr "Quelle" + +msgid "Space freed: %s" +msgstr "Freigewordener Platz: %s" + +msgid "Start after creation" +msgstr "Nach Erstellung starten" + +msgid "Start container" +msgstr "Container starten" + +msgid "Start on boot" +msgstr "Beim Systemstart starten" + +msgid "Start script disabled" +msgstr "Startskript deaktiviert" + +msgid "Start script missing\n\nClick to generate" +msgstr "Startskript fehlt\n\nKlicken zum Generieren" + +msgid "Start stream" +msgstr "Stream starten" + +msgid "Started" +msgstr "Gestartet" + +msgid "Started At" +msgstr "Gestartet um" + +msgid "Starting pod" +msgstr "Pod wird gestartet" + +msgid "Statistics" +msgstr "Statistiken" + +msgid "Stats" +msgstr "Stats" + +msgid "Status" +msgstr "Status" + +msgid "Stop container" +msgstr "Container stoppen" + +msgid "Stop stream" +msgstr "Stream stoppen" + +msgid "Stopping pod" +msgstr "Pod wird gestoppt" + +msgid "Subnet" +msgstr "Subnetz" + +msgid "Supplementary Groups" +msgstr "Zusätzliche Gruppen" + +msgid "System Diagnostics" +msgstr "Systemdiagnose" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "Tag" + +msgid "The sensitive data to store securely" +msgstr "Die sensiblen Daten, die sicher gespeichert werden sollen" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "Für automatische Aktualisierung das Label hinzufügen: io.containers.autoupdate=registry" + +msgid "Total" +msgstr "Gesamt" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "Gesamtspeicherlimit (Speicher + Swap). -1 = unbegrenzt, 0 = Standard." + +msgid "Type" +msgstr "Typ" + +msgid "Unknown" +msgstr "Unbekannt" + +msgid "Unless Stopped" +msgstr "Sofern nicht gestoppt" + +msgid "Until" +msgstr "Bis" + +msgid "Update" +msgstr "Aktualisieren" + +msgid "Update Resources" +msgstr "Ressourcen aktualisieren" + +msgid "Update name" +msgstr "Name aktualisieren" + +msgid "Update restart policy" +msgstr "Neustart-Richtlinie aktualisieren" + +msgid "Updating Images" +msgstr "Images werden aktualisiert" + +msgid "Updating container" +msgstr "Container wird aktualisiert" + +msgid "Upload failed: %s" +msgstr "Upload fehlgeschlagen: %s" + +msgid "Uploading file..." +msgstr "Datei wird hochgeladen..." + +msgid "User" +msgstr "Benutzer" + +msgid "User and group to run as (UID:GID)" +msgstr "Benutzer und Gruppe (UID:GID)" + +msgid "Value" +msgstr "Wert" + +msgid "Variable" +msgstr "Variable" + +msgid "Volume" +msgstr "Volume" + +msgid "Volume Name" +msgstr "Volume-Name" + +msgid "Volume driver to use" +msgstr "Zu verwendender Volume-Treiber" + +msgid "Volume imported successfully" +msgstr "Volume erfolgreich importiert" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "Volume-Name. Leer lassen für automatische Generierung." + +msgid "Volumes" +msgstr "Volumes" + +msgid "Working Directory" +msgstr "Arbeitsverzeichnis" + +msgid "Yes" +msgstr "Ja" + +msgid "default" +msgstr "Standard" + +msgid "non-empty value" +msgstr "nicht leerer Wert" + +msgid "running" +msgstr "läuft" + +msgid "total" +msgstr "gesamt" + +msgid "Are you sure to remove pod?" +msgstr "Pod wirklich entfernen?" + +msgid "Cgroup parent" +msgstr "Cgroup-Parent" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "CPU-%" + +msgid "CPU period" +msgstr "CPU-Periode" + +msgid "CPU quota" +msgstr "CPU-Quote" + +msgid "CPU set" +msgstr "CPU-Set" + +msgid "CPU shares" +msgstr "CPU-Shares" + +msgid "DNS options" +msgstr "DNS-Optionen" + +msgid "DNS search" +msgstr "DNS-Suche" + +msgid "DNS servers" +msgstr "DNS-Server" + +msgid "Exit policy" +msgstr "Exit-Policy" + +msgid "Extra hosts" +msgstr "Zusätzliche Hosts" + +msgid "Host network" +msgstr "Host-Netzwerk" + +msgid "Infra container" +msgstr "Infra-Container" + +msgid "Memory limit" +msgstr "Speicherlimit" + +msgid "Net I/O" +msgstr "Netzwerk-I/O" + +msgid "Pause pod" +msgstr "Pod pausieren" + +msgid "Pod is not running" +msgstr "Pod läuft nicht" + +msgid "Remove pod" +msgstr "Pod entfernen" + +msgid "Restart policy" +msgstr "Restart-Policy" + +msgid "Restart pod" +msgstr "Pod neustarten" + +msgid "Shared namespaces" +msgstr "Gemeinsame Namespaces" + +msgid "Start pod" +msgstr "Pod starten" + +msgid "Static IPv4" +msgstr "Statische IPv4" + +msgid "Static MAC" +msgstr "Statische MAC" + +msgid "Stop pod" +msgstr "Pod stoppen" + +msgid "Unlimited" +msgstr "Unbegrenzt" + +msgid "Unpause pod" +msgstr "Pod fortsetzen" + +msgid "Stopping container" +msgstr "Container wird gestoppt" + +msgid "Stop failed: %s" +msgstr "Stoppen fehlgeschlagen: %s" + +msgid "Removing container" +msgstr "Container wird entfernt" + +msgid "Remove failed: %s" +msgstr "Entfernen fehlgeschlagen: %s" + +msgid "Creating container" +msgstr "Container wird erstellt" + +msgid "Create failed: %s" +msgstr "Erstellen fehlgeschlagen: %s" + +msgid "Starting container" +msgstr "Container wird gestartet" + +msgid "Start failed: %s" +msgstr "Start fehlgeschlagen: %s" + +msgid "Reinstating init script" +msgstr "Init-Skript wird wiederhergestellt" + +msgid "Init script reinstate failed: %s" +msgstr "Wiederherstellen des Init-Skripts fehlgeschlagen: %s" + +msgid "Updated to %s" +msgstr "Auf %s aktualisiert" + +msgid "Update image: %d/%d - %s" +msgstr "Image aktualisieren: %d/%d - %s" + +msgid "Removed old image %s" +msgstr "Altes Image %s entfernt" + +msgid "Done." +msgstr "Fertig." + +msgid "Pause container" +msgstr "Container pausieren" + +msgid "Unpause container" +msgstr "Container fortsetzen" diff --git a/luci-app-podman/po/es/podman.po b/luci-app-podman/po/es/podman.po new file mode 100644 index 000000000..129901400 --- /dev/null +++ b/luci-app-podman/po/es/podman.po @@ -0,0 +1,1090 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-05-03 11:05+0000\n" +"Last-Translator:\n" +"Language-Team: Dutch \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "%d check(s) failed:" +msgstr "%d comprobación(es) fallida(s):" + +msgid "%d updated, %d failed:" +msgstr "%d actualizados, %d fallidos:" + +msgid "%s created successfully" +msgstr "%s creado correctamente" + +msgid "%s: %d/%d" +msgstr "%s: %d/%d" + +msgid "(none)" +msgstr "(ninguno)" + +msgid "-- Select %s --" +msgstr "-- Seleccionar %s --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 caracteres: solo letras, números, guion bajo (_) y guion (-)" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m, 1g o -1 para ilimitado" + +msgid "API Version" +msgstr "Versión de la API" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "Añadir el contenedor a un pod existente. La red, el nombre de host y los mapeos de puertos provienen del contenedor infra del pod." + +msgid "All images updated successfully" +msgstr "Todas las imágenes actualizadas correctamente" + +msgid "All volumes exported successfully" +msgstr "Todos los volúmenes se exportaron correctamente" + +msgid "Allocate TTY (-t)" +msgstr "Asignar TTY (-t)" + +msgid "Allocate container IP from this range" +msgstr "Asignar IP del contenedor desde este rango" + +msgid "Always" +msgstr "Siempre" + +msgid "Apply Updates" +msgstr "Aplicar actualizaciones" + +msgid "Are you sure to delete the records?" +msgstr "¿Eliminar los registros?" + +msgid "Are you sure to remove container?" +msgstr "¿Eliminar el contenedor?" + +msgid "Are you sure to remove integration?" +msgstr "¿Eliminar la integración?" + +msgid "Auto Remove (--rm)" +msgstr "Eliminación automática (--rm)" + +msgid "Auto Update" +msgstr "Actualización automática" + +msgid "Auto-Update" +msgstr "Actualización automática" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "Configura automáticamente la interfaz de red, el puente y la zona de firewall de OpenWrt. Muy recomendado para una red de contenedores adecuada en OpenWrt." + +msgid "Automatically start the container after it is created" +msgstr "Iniciar automáticamente el contenedor después de crearlo" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "Actualizar automáticamente el contenedor cuando haya una imagen más reciente. Añade la etiqueta: io.containers.autoupdate=registry" + +msgid "Basic Information" +msgstr "Información básica" + +msgid "Block I/O" +msgstr "E/S de bloque" + +msgid "Boot" +msgstr "Arranque" + +msgid "Bridge Interface Name" +msgstr "Nombre de la interfaz puente" + +msgid "CPU" +msgstr "CPU" + +msgid "CPU Limit" +msgstr "Límite de CPU" + +msgid "CPU Set" +msgstr "Conjunto de CPU" + +msgid "CPU Shares Weight" +msgstr "Peso de CPU shares" + +msgid "CPU Usage" +msgstr "Uso de CPU" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "Cuotas de CPU (peso relativo). 0 = usar predeterminado." + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "CPUs en las que el pod puede ejecutarse (p. ej., \"0-3\" o \"0,2\")" + +msgid "Cancel" +msgstr "Cancelar" + +msgid "Check" +msgstr "Comprobar" + +msgid "Check %d containers for updates?" +msgstr "¿Comprobar actualizaciones de %d contenedores?" + +msgid "Check Complete" +msgstr "Comprobación completa" + +msgid "Check for Updates" +msgstr "Buscar actualizaciones" + +msgid "Check for updates: %s/%s" +msgstr "Buscando actualizaciones: %s/%s" + +msgid "Choose firewall zone for this network." +msgstr "Elegir zona de firewall para esta red." + +msgid "Clean Up Now" +msgstr "Limpiar ahora" + +msgid "Cleaning up old images..." +msgstr "Eliminando imágenes antiguas..." + +msgid "Cleanup / Prune" +msgstr "Limpiar / Purgar" + +msgid "Cleanup Complete" +msgstr "Limpieza completada" + +msgid "Cleanup Unused Resources" +msgstr "Limpiar recursos no utilizados" + +msgid "Cleanup successful!" +msgstr "¡Limpieza exitosa!" + +msgid "Clear logs" +msgstr "Borrar registros" + +msgid "Click icon to repair" +msgstr "Haz clic en el icono para reparar" + +msgid "Click to disconnect" +msgstr "Clic para desconectar" + +msgid "Click to enable/disable" +msgstr "Clic para habilitar/deshabilitar" + +msgid "Click to reveal/hide value" +msgstr "Haga clic para mostrar/ocultar el valor" + +msgid "Close" +msgstr "Cerrar" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "Lista de IDs de grupos suplementarios separados por coma" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "Puertos a exponer separados por coma (ej. 6052, 8080/udp)" + +msgid "Command" +msgstr "Comando" + +msgid "Command to run (space-separated)" +msgstr "Comando a ejecutar (separado por espacios)" + +msgid "Configuration" +msgstr "Configuración" + +msgid "Configure resource limits for this container." +msgstr "Configurar límites de recursos para este contenedor." + +msgid "Confirm" +msgstr "Confirmar" + +msgid "Connect" +msgstr "Conectar" + +msgid "Connect to" +msgstr "Conectar a" + +msgid "Connected successfully to %s" +msgstr "Conectado correctamente a %s" + +msgid "Connecting container to network" +msgstr "Conectando contenedor a la red" + +msgid "Console" +msgstr "Consola" + +msgid "Container" +msgstr "Contenedor" + +msgid "Container Name" +msgstr "Nombre del contenedor" + +msgid "Container image has no tag - cannot update" +msgstr "La imagen del contenedor no tiene etiqueta - no se puede actualizar" + +msgid "Container image to use" +msgstr "Imagen de contenedor a usar" + +msgid "Container is not running" +msgstr "El contenedor no está en ejecución" + +msgid "Container updated successfully" +msgstr "Contenedor actualizado correctamente" + +msgid "Containers" +msgstr "Contenedores" + +msgid "Containers in pod %s" +msgstr "Contenedores en el pod %s" + +msgid "Containers updated successfully" +msgstr "Contenedores actualizados correctamente" + +msgid "Containers updated successfully." +msgstr "Contenedores actualizados correctamente." + +msgid "Create" +msgstr "Crear" + +msgid "Create %s" +msgstr "Crear %s" + +msgid "Create Command" +msgstr "Comando de creación" + +msgid "Create Infra Container" +msgstr "Crear contenedor infra" + +msgid "Create new zone (will be named: podman_)" +msgstr "Crear nueva zona (se llamará: podman_)" + +msgid "Created" +msgstr "Creado" + +msgid "Creating %s" +msgstr "Creando %s" + +msgid "Creating auto-start configuration for %s" +msgstr "Creando configuración de inicio automático para %s" + +msgid "DNS Search Domains" +msgstr "Dominios de búsqueda DNS" + +msgid "DNS Servers" +msgstr "Servidores DNS" + +msgid "DNS search domains for the infra container" +msgstr "Dominios de búsqueda DNS para el contenedor infra" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "Servidores DNS para el contenedor infra (compartidos con todos los contenedores del pod)" + +msgid "Delete" +msgstr "Eliminar" + +msgid "Deleting records: %d/%d" +msgstr "Eliminando registros: %d/%d" + +msgid "Deleting records: %s/%s" +msgstr "Eliminando registros: %s/%s" + +msgid "Destination" +msgstr "Destino" + +msgid "Device" +msgstr "Dispositivo" + +msgid "Disabled" +msgstr "Desactivado" + +msgid "Disabling init script" +msgstr "Desactivando script de inicio" + +msgid "Disconnect container from network" +msgstr "Desconectar contenedor de la red" + +msgid "Disconnected from %s" +msgstr "Desconectado de %s" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "Controlador" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "Opciones específicas del controlador (separadas por comas, ej. type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "Activar IPv6" + +msgid "Enable IPv6 networking" +msgstr "Activar red IPv6" + +msgid "Enabling init script" +msgstr "Activando script de inicio" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "Introduzca datos secretos (contraseña, token, clave, etc.)" + +msgid "Entrypoint" +msgstr "Punto de entrada (Entrypoint)" + +msgid "Environment Variables" +msgstr "Variables de entorno" + +msgid "Ephemeral (--rm flag)" +msgstr "Efímero (opción --rm)" + +msgid "Error" +msgstr "Error" + +msgid "Error: %s" +msgstr "Error: %s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "Interfaz física existente para usar como padre. Requerido para redes macvlan/ipvlan." + +msgid "Expecting: %s" +msgstr "Se espera: %s" + +msgid "Export" +msgstr "Exportar" + +msgid "Exporting selected volumes: %d/%d" +msgstr "Exportando volúmenes seleccionados: %d/%d" + +msgid "Expose Ports" +msgstr "Exponer puertos" + +msgid "Failed to create container" +msgstr "Error al crear el contenedor" + +msgid "Fetching information..." +msgstr "Obteniendo información..." + +msgid "Find updateable containers" +msgstr "Buscar contenedores actualizables" + +msgid "Firewall Zone" +msgstr "Zona de firewall" + +msgid "Gateway" +msgstr "Puerta de enlace" + +msgid "Generate" +msgstr "Generar" + +msgid "Graph Root" +msgstr "Raíz del gráfico (Graph Root)" + +msgid "Health" +msgstr "Salud" + +msgid "Hostname" +msgstr "Nombre de host" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Nombre de host del pod (por defecto, el nombre del pod)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "Dirección IP" + +msgid "IP Range (CIDR)" +msgstr "Rango de IP (CIDR)" + +msgid "IPv4 Gateway" +msgstr "Puerta de enlace IPv4" + +msgid "IPv4 Subnet (CIDR)" +msgstr "Subred IPv4 (CIDR)" + +msgid "IPv4 gateway address" +msgstr "Dirección de puerta de enlace IPv4" + +msgid "IPv4 subnet in CIDR notation" +msgstr "Subred IPv4 en notación CIDR" + +msgid "Image" +msgstr "Imagen" + +msgid "Image ID" +msgstr "ID de imagen" + +msgid "Image is already up-to-date" +msgstr "La imagen ya está actualizada" + +msgid "Image pulled successfully" +msgstr "Imagen descargada correctamente" + +msgid "Images" +msgstr "Imágenes" + +msgid "Import" +msgstr "Importar" + +msgid "Import from archive: %s" +msgstr "Importar desde archivo: %s" + +msgid "Importing volume..." +msgstr "Importando volumen..." + +msgid "Info" +msgstr "Info" + +msgid "Information" +msgstr "Información" + +msgid "Infra Image" +msgstr "Imagen infra" + +msgid "Init Script" +msgstr "Script de inicio" + +msgid "Inspect" +msgstr "Inspeccionar" + +msgid "Int" +msgstr "Ent" + +msgid "Interactive" +msgstr "Interactivo" + +msgid "Interface" +msgstr "Interfaz" + +msgid "Internal Network" +msgstr "Red interna" + +msgid "Labels" +msgstr "Etiquetas" + +msgid "Labels in key=value format, one per line" +msgstr "Etiquetas en formato clave=valor, una por línea" + +msgid "Leave empty to auto-generate" +msgstr "Dejar vacío para generar automáticamente" + +msgid "Links" +msgstr "Enlaces" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Namespaces de Linux compartidos entre los contenedores del pod. El valor por defecto coincide con `podman pod create`." + +msgid "Load" +msgstr "Cargar" + +msgid "Load System diagnostics" +msgstr "Cargar diagnósticos del sistema" + +msgid "Loading init script" +msgstr "Cargando script de inicio" + +msgid "Loading..." +msgstr "Cargando..." + +msgid "Logs" +msgstr "Registros" + +msgid "Manage podman %s" +msgstr "Gestionar podman %s" + +msgid "Memory" +msgstr "Memoria" + +msgid "Memory %" +msgstr "Memoria %" + +msgid "Memory + Swap Limit" +msgstr "Límite de memoria + intercambio" + +msgid "Memory Limit" +msgstr "Límite de memoria" + +msgid "Memory Usage" +msgstr "Uso de memoria" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "Límite de memoria (ej. 512m, 1g)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "Límite de memoria (ej. 512m, 1g). -1 = ilimitado, 0 = predeterminado." + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Límite de memoria para el pod (p. ej., 512m, 1g)" + +msgid "Mode" +msgstr "Modo" + +msgid "Mount Options" +msgstr "Opciones de montaje" + +msgid "Mountpoint" +msgstr "Punto de montaje" + +msgid "Mounts" +msgstr "Montajes" + +msgid "Must be between 0 and 262144" +msgstr "Debe estar entre 0 y 262144" + +msgid "Name" +msgstr "Nombre" + +msgid "Name for the network" +msgstr "Nombre para la red" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "Nombre de la interfaz puente (ej. podman0, mynet0). Dejar vacío para usar: <nombre-de-red>0. Nota: Si el nombre entra en conflicto con uno existente, OpenWrt lo incrementará." + +msgid "Name updated successfully" +msgstr "Nombre actualizado correctamente" + +msgid "Network" +msgstr "Red" + +msgid "Network I/O" +msgstr "E/S de red" + +msgid "Network Mode" +msgstr "Modo de red" + +msgid "Network Name" +msgstr "Nombre de la red" + +msgid "Network driver" +msgstr "Controlador de red" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Namespace de red para el pod. Los contenedores del pod comparten esta red." + +msgid "Networks" +msgstr "Redes" + +msgid "Never" +msgstr "Nunca" + +msgid "No" +msgstr "No" + +msgid "No %s selected" +msgstr "No hay %s seleccionado" + +msgid "No containers in this pod" +msgstr "No hay contenedores en este pod" + +msgid "No containers with auto-update label found." +msgstr "No se encontraron contenedores con la etiqueta de actualización automática." + +msgid "No new updates available." +msgstr "No hay nuevas actualizaciones disponibles." + +msgid "No suitable interfaces found" +msgstr "No se encontraron interfaces adecuadas" + +msgid "No unused resources found" +msgstr "No se encontraron recursos sin usar" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "Número de CPUs (ej. 0.5, 1.0, 2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "Número de CPUs (ej. 0.5, 1.0, 2.0). Dejar vacío para ilimitado." + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Número de CPUs disponibles para el pod (p. ej., 0.5, 1.0, 2.0)" + +msgid "Number of lines" +msgstr "Número de líneas" + +msgid "OK" +msgstr "Aceptar" + +msgid "Old image still used by other containers" +msgstr "La imagen antigua aún la usan otros contenedores" + +msgid "On Failure" +msgstr "En caso de fallo" + +msgid "One per line, format: host:container[/protocol]" +msgstr "Uno por línea, formato: host:contenedor[/protocolo]" + +msgid "One per line, format: key=value" +msgstr "Uno por línea, formato: clave=valor" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "Uno por línea. Formato: origen:destino[:opciones]. Opciones: ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "Integración OpenWrt existente\n\nClic para eliminar" + +msgid "OpenWrt integration incomplete" +msgstr "Integración OpenWrt incompleta" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "La integración de OpenWrt requiere especificar una subred" + +msgid "Override the image used for the infra container" +msgstr "Sobrescribir la imagen utilizada para el contenedor infra" + +msgid "PIDs" +msgstr "PIDs" + +msgid "Parent Interface" +msgstr "Interfaz padre" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "La interfaz padre es obligatoria para redes macvlan/ipvlan" + +msgid "Part of a pod (\"%s\")" +msgstr "Parte de un pod (\"%s\")" + +msgid "Pausing pod" +msgstr "Pausando pod" + +msgid "Pod" +msgstr "Pod" + +msgid "Pod Name" +msgstr "Nombre del pod" + +msgid "Podman Version" +msgstr "Versión de Podman" + +msgid "Pods" +msgstr "Pods" + +msgid "Port Mappings" +msgstr "Mapeo de puertos" + +msgid "Ports" +msgstr "Puertos" + +msgid "Privileged" +msgstr "Privilegiado" + +msgid "Privileged Mode" +msgstr "Modo privilegiado" + +msgid "Processes" +msgstr "Procesos" + +msgid "Pull Image" +msgstr "Descargar imagen" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "El pull no devolvió un ID de imagen - el pull puede haber fallado" + +msgid "Pulling %s" +msgstr "Descargando %s" + +msgid "Read: %s / Write: %s" +msgstr "Lectura: %s / Escritura: %s" + +msgid "Refreshing %s..." +msgstr "Actualizando %s..." + +msgid "Registries" +msgstr "Registros (Registries)" + +msgid "Registry" +msgstr "Registro" + +msgid "Reload" +msgstr "Recargar" + +msgid "Remove OpenWrt integration" +msgstr "Eliminar integración OpenWrt" + +msgid "Remove all unused images (not just dangling)" +msgstr "Eliminar todas las imágenes sin usar (no solo las huérfanas)" + +msgid "Remove container" +msgstr "Eliminar contenedor" + +msgid "Remove unused volumes" +msgstr "Eliminar volúmenes sin usar" + +msgid "Removed: %s" +msgstr "Eliminado: %s" + +msgid "Removing unused resources, please wait..." +msgstr "Eliminando recursos sin usar, espere..." + +msgid "Repository" +msgstr "Repositorio" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "Requerido para el namespace de red compartido. Desactivar solo si está seguro de no necesitarlo." + +msgid "Resources" +msgstr "Recursos" + +msgid "Restart Policy" +msgstr "Política de reinicio" + +msgid "Restart container" +msgstr "Reiniciar contenedor" + +msgid "Restart policy updated successfully" +msgstr "Política de reinicio actualizada correctamente" + +msgid "Restarting pod" +msgstr "Reiniciando pod" + +msgid "Restrict external access to the network" +msgstr "Restringir el acceso externo a la red" + +msgid "Run Root" +msgstr "Raíz de ejecución (Run Root)" + +msgid "Saving resource" +msgstr "Guardando recurso" + +msgid "Secret" +msgstr "Secreto (Secret)" + +msgid "Secret Data" +msgstr "Datos secretos" + +msgid "Secret Name" +msgstr "Nombre del secreto" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "El nombre del secreto solo puede contener letras, números, guiones bajos y guiones" + +msgid "Secrets" +msgstr "Secretos" + +msgid "Security Notice:" +msgstr "Aviso de seguridad:" + +msgid "Select containers to update" +msgstr "Seleccionar contenedores para actualizar" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "Seleccionar red para el nuevo contenedor. Las redes de usuario ofrecen mejor aislamiento y resolución DNS entre contenedores." + +msgid "Select what to clean up:" +msgstr "Seleccione qué limpiar:" + +msgid "Sensitive data is hidden for security reasons." +msgstr "Los datos sensibles están ocultos por razones de seguridad." + +msgid "Setup OpenWrt Integration" +msgstr "Configurar integración de OpenWrt" + +msgid "Shared Namespaces" +msgstr "Namespaces compartidos" + +msgid "Shares network with another container" +msgstr "Comparte red con otro contenedor" + +msgid "Show" +msgstr "Mostrar" + +msgid "Since" +msgstr "Desde" + +msgid "Size" +msgstr "Tamaño" + +msgid "Socket Path" +msgstr "Ruta del socket" + +msgid "Source" +msgstr "Origen" + +msgid "Space freed: %s" +msgstr "Espacio liberado: %s" + +msgid "Start after creation" +msgstr "Iniciar tras la creación" + +msgid "Start container" +msgstr "Iniciar contenedor" + +msgid "Start on boot" +msgstr "Iniciar al arrancar" + +msgid "Start script disabled" +msgstr "Script de inicio desactivado" + +msgid "Start script missing\n\nClick to generate" +msgstr "Script de inicio faltante\n\nHaga clic para generar" + +msgid "Start stream" +msgstr "Iniciar transmisión" + +msgid "Started" +msgstr "Iniciado" + +msgid "Started At" +msgstr "Iniciado en" + +msgid "Starting pod" +msgstr "Iniciando pod" + +msgid "Statistics" +msgstr "Estadísticas" + +msgid "Stats" +msgstr "Estadísticas" + +msgid "Status" +msgstr "Estado" + +msgid "Stop container" +msgstr "Detener contenedor" + +msgid "Stop stream" +msgstr "Detener transmisión" + +msgid "Stopping pod" +msgstr "Deteniendo pod" + +msgid "Subnet" +msgstr "Subred" + +msgid "Supplementary Groups" +msgstr "Grupos suplementarios" + +msgid "System Diagnostics" +msgstr "Diagnósticos del sistema" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "Etiqueta" + +msgid "The sensitive data to store securely" +msgstr "Los datos sensibles a almacenar de forma segura" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "Para habilitar la actualización automática, añadir la etiqueta: io.containers.autoupdate=registry" + +msgid "Total" +msgstr "Total" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "Límite de memoria total (memoria + swap). -1 = ilimitado, 0 = predeterminado." + +msgid "Type" +msgstr "Tipo" + +msgid "Unknown" +msgstr "Desconocido" + +msgid "Unless Stopped" +msgstr "A menos que se detenga (Unless Stopped)" + +msgid "Until" +msgstr "Hasta" + +msgid "Update" +msgstr "Actualizar" + +msgid "Update Resources" +msgstr "Actualizar recursos" + +msgid "Update name" +msgstr "Actualizar nombre" + +msgid "Update restart policy" +msgstr "Actualizar política de reinicio" + +msgid "Updating Images" +msgstr "Actualizando imágenes" + +msgid "Updating container" +msgstr "Actualizando contenedor" + +msgid "Upload failed: %s" +msgstr "Error al subir: %s" + +msgid "Uploading file..." +msgstr "Subiendo archivo..." + +msgid "User" +msgstr "Usuario" + +msgid "User and group to run as (UID:GID)" +msgstr "Usuario y grupo (UID:GID)" + +msgid "Value" +msgstr "Valor" + +msgid "Variable" +msgstr "Variable" + +msgid "Volume" +msgstr "Volumen" + +msgid "Volume Name" +msgstr "Nombre del volumen" + +msgid "Volume driver to use" +msgstr "Controlador de volumen a usar" + +msgid "Volume imported successfully" +msgstr "Volumen importado correctamente" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "Nombre del volumen. Dejar vacío para generar automáticamente." + +msgid "Volumes" +msgstr "Volúmenes" + +msgid "Working Directory" +msgstr "Directorio de trabajo" + +msgid "Yes" +msgstr "Sí" + +msgid "default" +msgstr "predeterminado" + +msgid "non-empty value" +msgstr "valor no vacío" + +msgid "running" +msgstr "en ejecución" + +msgid "total" +msgstr "total" + +msgid "Are you sure to remove pod?" +msgstr "¿Está seguro de eliminar el pod?" + +msgid "Cgroup parent" +msgstr "Cgroup padre" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "CPU %" + +msgid "CPU period" +msgstr "Período de CPU" + +msgid "CPU quota" +msgstr "Cuota de CPU" + +msgid "CPU set" +msgstr "Conjunto de CPU" + +msgid "CPU shares" +msgstr "Shares de CPU" + +msgid "DNS options" +msgstr "Opciones DNS" + +msgid "DNS search" +msgstr "Búsqueda DNS" + +msgid "DNS servers" +msgstr "Servidores DNS" + +msgid "Exit policy" +msgstr "Política de salida" + +msgid "Extra hosts" +msgstr "Hosts adicionales" + +msgid "Host network" +msgstr "Red del host" + +msgid "Infra container" +msgstr "Contenedor infra" + +msgid "Memory limit" +msgstr "Límite de memoria" + +msgid "Net I/O" +msgstr "I/O de red" + +msgid "Pause pod" +msgstr "Pausar pod" + +msgid "Pod is not running" +msgstr "El pod no está en ejecución" + +msgid "Remove pod" +msgstr "Eliminar pod" + +msgid "Restart policy" +msgstr "Política de reinicio" + +msgid "Restart pod" +msgstr "Reiniciar pod" + +msgid "Shared namespaces" +msgstr "Namespaces compartidos" + +msgid "Start pod" +msgstr "Iniciar pod" + +msgid "Static IPv4" +msgstr "IPv4 estática" + +msgid "Static MAC" +msgstr "MAC estática" + +msgid "Stop pod" +msgstr "Detener pod" + +msgid "Unlimited" +msgstr "Ilimitado" + +msgid "Unpause pod" +msgstr "Reanudar pod" + +msgid "Stopping container" +msgstr "Deteniendo contenedor" + +msgid "Stop failed: %s" +msgstr "Error al detener: %s" + +msgid "Removing container" +msgstr "Eliminando contenedor" + +msgid "Remove failed: %s" +msgstr "Error al eliminar: %s" + +msgid "Creating container" +msgstr "Creando contenedor" + +msgid "Create failed: %s" +msgstr "Error al crear: %s" + +msgid "Starting container" +msgstr "Iniciando contenedor" + +msgid "Start failed: %s" +msgstr "Error al iniciar: %s" + +msgid "Reinstating init script" +msgstr "Restableciendo script de inicio" + +msgid "Init script reinstate failed: %s" +msgstr "Error al restablecer el script de inicio: %s" + +msgid "Updated to %s" +msgstr "Actualizado a %s" + +msgid "Update image: %d/%d - %s" +msgstr "Actualizar imagen: %d/%d - %s" + +msgid "Removed old image %s" +msgstr "Imagen antigua %s eliminada" + +msgid "Done." +msgstr "Listo." + +msgid "Pause container" +msgstr "Pausar contenedor" + +msgid "Unpause container" +msgstr "Reanudar contenedor" diff --git a/luci-app-podman/po/fr/podman.po b/luci-app-podman/po/fr/podman.po new file mode 100644 index 000000000..ca943674a --- /dev/null +++ b/luci-app-podman/po/fr/podman.po @@ -0,0 +1,1090 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-05-03 11:05+0000\n" +"Last-Translator:\n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "%d check(s) failed:" +msgstr "%d vérification(s) échouée(s) :" + +msgid "%d updated, %d failed:" +msgstr "%d mis à jour, %d échoué(s) :" + +msgid "%s created successfully" +msgstr "%s créé avec succès" + +msgid "%s: %d/%d" +msgstr "%s : %d/%d" + +msgid "(none)" +msgstr "(aucun)" + +msgid "-- Select %s --" +msgstr "-- Sélectionner %s --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 caractères : lettres, chiffres, underscore (_) et trait d'union (-) uniquement" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m, 1g ou -1 pour illimité" + +msgid "API Version" +msgstr "Version de l'API" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "Ajouter le conteneur à un pod existant. Le réseau, le nom d'hôte et les mappages de ports proviennent du conteneur infra du pod." + +msgid "All images updated successfully" +msgstr "Toutes les images mises à jour avec succès" + +msgid "All volumes exported successfully" +msgstr "Tous les volumes ont été exportés avec succès" + +msgid "Allocate TTY (-t)" +msgstr "Allouer un TTY (-t)" + +msgid "Allocate container IP from this range" +msgstr "Allouer l'IP du conteneur à partir de cette plage" + +msgid "Always" +msgstr "Toujours" + +msgid "Apply Updates" +msgstr "Appliquer les mises à jour" + +msgid "Are you sure to delete the records?" +msgstr "Supprimer les enregistrements ?" + +msgid "Are you sure to remove container?" +msgstr "Supprimer le conteneur ?" + +msgid "Are you sure to remove integration?" +msgstr "Supprimer l'intégration ?" + +msgid "Auto Remove (--rm)" +msgstr "Suppression automatique (--rm)" + +msgid "Auto Update" +msgstr "Mise à jour automatique" + +msgid "Auto-Update" +msgstr "Mise à jour automatique" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "Configurer automatiquement l'interface réseau, le pont et la zone de pare-feu OpenWrt. Fortement recommandé pour un réseau de conteneurs correct sur OpenWrt." + +msgid "Automatically start the container after it is created" +msgstr "Démarrer automatiquement le conteneur après sa création" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "Mettre à jour automatiquement le conteneur lorsqu'une image plus récente est disponible. Ajoute l'étiquette : io.containers.autoupdate=registry" + +msgid "Basic Information" +msgstr "Informations de base" + +msgid "Block I/O" +msgstr "E/S bloc" + +msgid "Boot" +msgstr "Démarrage" + +msgid "Bridge Interface Name" +msgstr "Nom de l'interface pont" + +msgid "CPU" +msgstr "CPU" + +msgid "CPU Limit" +msgstr "Limite CPU" + +msgid "CPU Set" +msgstr "Ensemble CPU" + +msgid "CPU Shares Weight" +msgstr "Poids des CPU shares" + +msgid "CPU Usage" +msgstr "Utilisation CPU" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "Parts CPU (poids relatif). 0 = valeur par défaut." + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "CPUs sur lesquels le pod peut s'exécuter (p. ex., \"0-3\" ou \"0,2\")" + +msgid "Cancel" +msgstr "Annuler" + +msgid "Check" +msgstr "Vérifier" + +msgid "Check %d containers for updates?" +msgstr "Vérifier les mises à jour de %d conteneurs ?" + +msgid "Check Complete" +msgstr "Vérification terminée" + +msgid "Check for Updates" +msgstr "Rechercher des mises à jour" + +msgid "Check for updates: %s/%s" +msgstr "Vérification des mises à jour : %s/%s" + +msgid "Choose firewall zone for this network." +msgstr "Choisir la zone de pare-feu pour ce réseau." + +msgid "Clean Up Now" +msgstr "Nettoyer maintenant" + +msgid "Cleaning up old images..." +msgstr "Nettoyage des anciennes images..." + +msgid "Cleanup / Prune" +msgstr "Nettoyage / Prune" + +msgid "Cleanup Complete" +msgstr "Nettoyage terminé" + +msgid "Cleanup Unused Resources" +msgstr "Nettoyer les ressources inutilisées" + +msgid "Cleanup successful!" +msgstr "Nettoyage réussi !" + +msgid "Clear logs" +msgstr "Effacer les journaux" + +msgid "Click icon to repair" +msgstr "Cliquer sur l'icône pour réparer" + +msgid "Click to disconnect" +msgstr "Cliquer pour déconnecter" + +msgid "Click to enable/disable" +msgstr "Cliquer pour activer/désactiver" + +msgid "Click to reveal/hide value" +msgstr "Cliquer pour afficher/masquer la valeur" + +msgid "Close" +msgstr "Fermer" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "Liste d'ID de groupes supplémentaires séparés par des virgules" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "Ports à exposer séparés par des virgules (ex. 6052, 8080/udp)" + +msgid "Command" +msgstr "Commande" + +msgid "Command to run (space-separated)" +msgstr "Commande à exécuter (séparée par des espaces)" + +msgid "Configuration" +msgstr "Configuration" + +msgid "Configure resource limits for this container." +msgstr "Configurer les limites de ressources pour ce conteneur." + +msgid "Confirm" +msgstr "Confirmer" + +msgid "Connect" +msgstr "Connecter" + +msgid "Connect to" +msgstr "Se connecter à" + +msgid "Connected successfully to %s" +msgstr "Connecté avec succès à %s" + +msgid "Connecting container to network" +msgstr "Connexion du conteneur au réseau" + +msgid "Console" +msgstr "Console" + +msgid "Container" +msgstr "Conteneur" + +msgid "Container Name" +msgstr "Nom du conteneur" + +msgid "Container image has no tag - cannot update" +msgstr "L'image du conteneur n'a pas de tag - impossible de mettre à jour" + +msgid "Container image to use" +msgstr "Image de conteneur à utiliser" + +msgid "Container is not running" +msgstr "Le conteneur n'est pas en cours d'exécution" + +msgid "Container updated successfully" +msgstr "Conteneur mis à jour avec succès" + +msgid "Containers" +msgstr "Conteneurs" + +msgid "Containers in pod %s" +msgstr "Conteneurs dans le pod %s" + +msgid "Containers updated successfully" +msgstr "Conteneurs mis à jour avec succès" + +msgid "Containers updated successfully." +msgstr "Conteneurs mis à jour avec succès." + +msgid "Create" +msgstr "Créer" + +msgid "Create %s" +msgstr "Créer %s" + +msgid "Create Command" +msgstr "Commande de création" + +msgid "Create Infra Container" +msgstr "Créer le conteneur infra" + +msgid "Create new zone (will be named: podman_)" +msgstr "Créer une nouvelle zone (sera nommée : podman_)" + +msgid "Created" +msgstr "Créé" + +msgid "Creating %s" +msgstr "Création de %s" + +msgid "Creating auto-start configuration for %s" +msgstr "Création de la configuration de démarrage automatique pour %s" + +msgid "DNS Search Domains" +msgstr "Domaines de recherche DNS" + +msgid "DNS Servers" +msgstr "Serveurs DNS" + +msgid "DNS search domains for the infra container" +msgstr "Domaines de recherche DNS pour le conteneur infra" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "Serveurs DNS pour le conteneur infra (partagés avec tous les conteneurs du pod)" + +msgid "Delete" +msgstr "Supprimer" + +msgid "Deleting records: %d/%d" +msgstr "Suppression des enregistrements : %d/%d" + +msgid "Deleting records: %s/%s" +msgstr "Suppression des enregistrements : %s/%s" + +msgid "Destination" +msgstr "Destination" + +msgid "Device" +msgstr "Périphérique" + +msgid "Disabled" +msgstr "Désactivé" + +msgid "Disabling init script" +msgstr "Désactivation du script d'initialisation" + +msgid "Disconnect container from network" +msgstr "Déconnecter le conteneur du réseau" + +msgid "Disconnected from %s" +msgstr "Déconnecté de %s" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "Pilote" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "Options spécifiques au pilote (séparées par des virgules, ex: type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "Activer IPv6" + +msgid "Enable IPv6 networking" +msgstr "Activer le réseau IPv6" + +msgid "Enabling init script" +msgstr "Activation du script d'initialisation" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "Saisir les données secrètes (mot de passe, jeton, clé, etc.)" + +msgid "Entrypoint" +msgstr "Point d'entrée (Entrypoint)" + +msgid "Environment Variables" +msgstr "Variables d'environnement" + +msgid "Ephemeral (--rm flag)" +msgstr "Éphémère (option --rm)" + +msgid "Error" +msgstr "Erreur" + +msgid "Error: %s" +msgstr "Erreur : %s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "Interface physique existante comme parent. Requis pour les réseaux macvlan/ipvlan." + +msgid "Expecting: %s" +msgstr "Attendu : %s" + +msgid "Export" +msgstr "Exporter" + +msgid "Exporting selected volumes: %d/%d" +msgstr "Exportation des volumes sélectionnés : %d/%d" + +msgid "Expose Ports" +msgstr "Exposer les ports" + +msgid "Failed to create container" +msgstr "Échec de la création du conteneur" + +msgid "Fetching information..." +msgstr "Récupération des informations..." + +msgid "Find updateable containers" +msgstr "Rechercher les conteneurs pouvant être mis à jour" + +msgid "Firewall Zone" +msgstr "Zone de pare-feu" + +msgid "Gateway" +msgstr "Passerelle" + +msgid "Generate" +msgstr "Générer" + +msgid "Graph Root" +msgstr "Racine du graphe" + +msgid "Health" +msgstr "Santé" + +msgid "Hostname" +msgstr "Nom d'hôte" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Nom d'hôte du pod (par défaut, le nom du pod)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "Adresse IP" + +msgid "IP Range (CIDR)" +msgstr "Plage IP (CIDR)" + +msgid "IPv4 Gateway" +msgstr "Passerelle IPv4" + +msgid "IPv4 Subnet (CIDR)" +msgstr "Sous-réseau IPv4 (CIDR)" + +msgid "IPv4 gateway address" +msgstr "Adresse de la passerelle IPv4" + +msgid "IPv4 subnet in CIDR notation" +msgstr "Sous-réseau IPv4 en notation CIDR" + +msgid "Image" +msgstr "Image" + +msgid "Image ID" +msgstr "ID de l'image" + +msgid "Image is already up-to-date" +msgstr "L'image est déjà à jour" + +msgid "Image pulled successfully" +msgstr "Image récupérée avec succès" + +msgid "Images" +msgstr "Images" + +msgid "Import" +msgstr "Importer" + +msgid "Import from archive: %s" +msgstr "Importer depuis l'archive : %s" + +msgid "Importing volume..." +msgstr "Importation du volume..." + +msgid "Info" +msgstr "Info" + +msgid "Information" +msgstr "Informations" + +msgid "Infra Image" +msgstr "Image infra" + +msgid "Init Script" +msgstr "Script d'initialisation" + +msgid "Inspect" +msgstr "Inspecter" + +msgid "Int" +msgstr "Ent" + +msgid "Interactive" +msgstr "Interactif" + +msgid "Interface" +msgstr "Interface" + +msgid "Internal Network" +msgstr "Réseau interne" + +msgid "Labels" +msgstr "Étiquettes" + +msgid "Labels in key=value format, one per line" +msgstr "Étiquettes au format clé=valeur, une par ligne" + +msgid "Leave empty to auto-generate" +msgstr "Laisser vide pour l'auto-génération" + +msgid "Links" +msgstr "Liens" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Namespaces Linux partagés entre les conteneurs du pod. La valeur par défaut correspond à `podman pod create`." + +msgid "Load" +msgstr "Charger" + +msgid "Load System diagnostics" +msgstr "Charger les diagnostics système" + +msgid "Loading init script" +msgstr "Chargement du script d'initialisation" + +msgid "Loading..." +msgstr "Chargement..." + +msgid "Logs" +msgstr "Journaux" + +msgid "Manage podman %s" +msgstr "Gérer podman %s" + +msgid "Memory" +msgstr "Mémoire" + +msgid "Memory %" +msgstr "Mémoire %" + +msgid "Memory + Swap Limit" +msgstr "Limite Mémoire + Swap" + +msgid "Memory Limit" +msgstr "Limite Mémoire" + +msgid "Memory Usage" +msgstr "Utilisation mémoire" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "Limite mémoire (ex: 512m, 1g)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "Limite mémoire (ex. 512m, 1g). -1 = illimité, 0 = par défaut." + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Limite de mémoire pour le pod (p. ex., 512m, 1g)" + +msgid "Mode" +msgstr "Mode" + +msgid "Mount Options" +msgstr "Options de montage" + +msgid "Mountpoint" +msgstr "Point de montage" + +msgid "Mounts" +msgstr "Montages" + +msgid "Must be between 0 and 262144" +msgstr "Doit être entre 0 et 262144" + +msgid "Name" +msgstr "Nom" + +msgid "Name for the network" +msgstr "Nom du réseau" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "Nom de l'interface pont (ex: podman0). Laisser vide pour utiliser : <nom-réseau>0. Note : En cas de conflit, OpenWrt l'incrémentera automatiquement." + +msgid "Name updated successfully" +msgstr "Nom mis à jour avec succès" + +msgid "Network" +msgstr "Réseau" + +msgid "Network I/O" +msgstr "E/S réseau" + +msgid "Network Mode" +msgstr "Mode réseau" + +msgid "Network Name" +msgstr "Nom du réseau" + +msgid "Network driver" +msgstr "Pilote réseau" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Namespace réseau pour le pod. Les conteneurs du pod partagent ce réseau." + +msgid "Networks" +msgstr "Réseaux" + +msgid "Never" +msgstr "Jamais" + +msgid "No" +msgstr "Non" + +msgid "No %s selected" +msgstr "Aucun %s sélectionné" + +msgid "No containers in this pod" +msgstr "Aucun conteneur dans ce pod" + +msgid "No containers with auto-update label found." +msgstr "Aucun conteneur avec l'étiquette de mise à jour automatique trouvé." + +msgid "No new updates available." +msgstr "Aucune nouvelle mise à jour disponible." + +msgid "No suitable interfaces found" +msgstr "Aucune interface appropriée trouvée" + +msgid "No unused resources found" +msgstr "Aucune ressource inutilisée trouvée" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "Nombre de CPUs (ex: 0.5, 1.0, 2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "Nombre de CPUs (ex: 0.5, 1.0, 2.0). Laisser vide pour illimité." + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Nombre de CPUs disponibles pour le pod (p. ex., 0.5, 1.0, 2.0)" + +msgid "Number of lines" +msgstr "Nombre de lignes" + +msgid "OK" +msgstr "OK" + +msgid "Old image still used by other containers" +msgstr "L'ancienne image est encore utilisée par d'autres conteneurs" + +msgid "On Failure" +msgstr "En cas d'échec" + +msgid "One per line, format: host:container[/protocol]" +msgstr "Un par ligne, format : hôte:conteneur[/protocole]" + +msgid "One per line, format: key=value" +msgstr "Une par ligne, format : clé=valeur" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "Un par ligne. Format : source:destination[:options]. Options : ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "Intégration OpenWrt existante\n\nCliquer pour supprimer" + +msgid "OpenWrt integration incomplete" +msgstr "Intégration OpenWrt incomplète" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "L'intégration OpenWrt nécessite la spécification d'un sous-réseau" + +msgid "Override the image used for the infra container" +msgstr "Remplacer l'image utilisée pour le conteneur infra" + +msgid "PIDs" +msgstr "PIDs" + +msgid "Parent Interface" +msgstr "Interface parent" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "L'interface parent est requise pour les réseaux macvlan/ipvlan" + +msgid "Part of a pod (\"%s\")" +msgstr "Fait partie du pod « %s »" + +msgid "Pausing pod" +msgstr "Mise en pause du pod" + +msgid "Pod" +msgstr "Pod" + +msgid "Pod Name" +msgstr "Nom du pod" + +msgid "Podman Version" +msgstr "Version de Podman" + +msgid "Pods" +msgstr "Pods" + +msgid "Port Mappings" +msgstr "Mappages de ports" + +msgid "Ports" +msgstr "Ports" + +msgid "Privileged" +msgstr "Privilégié" + +msgid "Privileged Mode" +msgstr "Mode privilégié" + +msgid "Processes" +msgstr "Processus" + +msgid "Pull Image" +msgstr "Récupérer l'image" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "Le pull n'a pas retourné d'ID d'image - le pull a peut-être échoué" + +msgid "Pulling %s" +msgstr "Téléchargement de %s" + +msgid "Read: %s / Write: %s" +msgstr "Lecture : %s / Écriture : %s" + +msgid "Refreshing %s..." +msgstr "Actualisation de %s..." + +msgid "Registries" +msgstr "Registres (Registries)" + +msgid "Registry" +msgstr "Registre" + +msgid "Reload" +msgstr "Recharger" + +msgid "Remove OpenWrt integration" +msgstr "Supprimer l'intégration OpenWrt" + +msgid "Remove all unused images (not just dangling)" +msgstr "Supprimer toutes les images inutilisées (pas seulement les orphelines)" + +msgid "Remove container" +msgstr "Supprimer le conteneur" + +msgid "Remove unused volumes" +msgstr "Supprimer les volumes inutilisés" + +msgid "Removed: %s" +msgstr "Supprimé : %s" + +msgid "Removing unused resources, please wait..." +msgstr "Suppression des ressources inutilisées, veuillez patienter..." + +msgid "Repository" +msgstr "Dépôt" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "Requis pour le namespace réseau partagé. Désactivez uniquement si vous savez que vous n'en avez pas besoin." + +msgid "Resources" +msgstr "Ressources" + +msgid "Restart Policy" +msgstr "Politique de redémarrage" + +msgid "Restart container" +msgstr "Redémarrer le conteneur" + +msgid "Restart policy updated successfully" +msgstr "Politique de redémarrage mise à jour avec succès" + +msgid "Restarting pod" +msgstr "Redémarrage du pod" + +msgid "Restrict external access to the network" +msgstr "Restreindre l'accès externe au réseau" + +msgid "Run Root" +msgstr "Run Root" + +msgid "Saving resource" +msgstr "Sauvegarde de la ressource" + +msgid "Secret" +msgstr "Secret" + +msgid "Secret Data" +msgstr "Données secrètes" + +msgid "Secret Name" +msgstr "Nom du secret" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "Le nom du secret ne peut contenir que des lettres, chiffres, underscores et tirets" + +msgid "Secrets" +msgstr "Secrets" + +msgid "Security Notice:" +msgstr "Avis de sécurité :" + +msgid "Select containers to update" +msgstr "Sélectionner les conteneurs à mettre à jour" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "Sélectionner le réseau pour le nouveau conteneur. Les réseaux utilisateur offrent un meilleur isolement et une meilleure résolution DNS entre conteneurs." + +msgid "Select what to clean up:" +msgstr "Sélectionner ce qu'il faut nettoyer :" + +msgid "Sensitive data is hidden for security reasons." +msgstr "Les données sensibles sont masquées pour des raisons de sécurité." + +msgid "Setup OpenWrt Integration" +msgstr "Configurer l'intégration OpenWrt" + +msgid "Shared Namespaces" +msgstr "Namespaces partagés" + +msgid "Shares network with another container" +msgstr "Partage le réseau avec un autre conteneur" + +msgid "Show" +msgstr "Afficher" + +msgid "Since" +msgstr "Depuis" + +msgid "Size" +msgstr "Taille" + +msgid "Socket Path" +msgstr "Chemin du socket" + +msgid "Source" +msgstr "Source" + +msgid "Space freed: %s" +msgstr "Espace libéré : %s" + +msgid "Start after creation" +msgstr "Démarrer après création" + +msgid "Start container" +msgstr "Démarrer le conteneur" + +msgid "Start on boot" +msgstr "Démarrer au démarrage" + +msgid "Start script disabled" +msgstr "Script de démarrage désactivé" + +msgid "Start script missing\n\nClick to generate" +msgstr "Script de démarrage manquant\n\nCliquez pour générer" + +msgid "Start stream" +msgstr "Démarrer le flux" + +msgid "Started" +msgstr "Démarré" + +msgid "Started At" +msgstr "Démarré à" + +msgid "Starting pod" +msgstr "Démarrage du pod" + +msgid "Statistics" +msgstr "Statistiques" + +msgid "Stats" +msgstr "Stats" + +msgid "Status" +msgstr "État" + +msgid "Stop container" +msgstr "Arrêter le conteneur" + +msgid "Stop stream" +msgstr "Arrêter le flux" + +msgid "Stopping pod" +msgstr "Arrêt du pod" + +msgid "Subnet" +msgstr "Sous-réseau" + +msgid "Supplementary Groups" +msgstr "Groupes supplémentaires" + +msgid "System Diagnostics" +msgstr "Diagnostics système" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "Tag" + +msgid "The sensitive data to store securely" +msgstr "Les données sensibles à stocker en toute sécurité" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "Pour activer la mise à jour automatique, ajouter l'étiquette : io.containers.autoupdate=registry" + +msgid "Total" +msgstr "Total" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "Limite mémoire totale (mémoire + swap). -1 = illimité, 0 = par défaut." + +msgid "Type" +msgstr "Type" + +msgid "Unknown" +msgstr "Inconnu" + +msgid "Unless Stopped" +msgstr "Sauf si arrêté (Unless Stopped)" + +msgid "Until" +msgstr "Jusqu'à" + +msgid "Update" +msgstr "Mettre à jour" + +msgid "Update Resources" +msgstr "Mettre à jour les ressources" + +msgid "Update name" +msgstr "Mettre à jour le nom" + +msgid "Update restart policy" +msgstr "Mettre à jour la politique de redémarrage" + +msgid "Updating Images" +msgstr "Mise à jour des images" + +msgid "Updating container" +msgstr "Mise à jour du conteneur" + +msgid "Upload failed: %s" +msgstr "Échec du téléchargement : %s" + +msgid "Uploading file..." +msgstr "Téléchargement du fichier..." + +msgid "User" +msgstr "Utilisateur" + +msgid "User and group to run as (UID:GID)" +msgstr "Utilisateur et groupe (UID:GID)" + +msgid "Value" +msgstr "Valeur" + +msgid "Variable" +msgstr "Variable" + +msgid "Volume" +msgstr "Volume" + +msgid "Volume Name" +msgstr "Nom du volume" + +msgid "Volume driver to use" +msgstr "Pilote de volume à utiliser" + +msgid "Volume imported successfully" +msgstr "Volume importé avec succès" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "Nom du volume. Laisser vide pour l'auto-génération." + +msgid "Volumes" +msgstr "Volumes" + +msgid "Working Directory" +msgstr "Répertoire de travail" + +msgid "Yes" +msgstr "Oui" + +msgid "default" +msgstr "par défaut" + +msgid "non-empty value" +msgstr "valeur non vide" + +msgid "running" +msgstr "en cours" + +msgid "total" +msgstr "total" + +msgid "Are you sure to remove pod?" +msgstr "Êtes-vous sûr de supprimer le pod ?" + +msgid "Cgroup parent" +msgstr "Cgroup parent" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "CPU %" + +msgid "CPU period" +msgstr "Période CPU" + +msgid "CPU quota" +msgstr "Quota CPU" + +msgid "CPU set" +msgstr "Ensemble CPU" + +msgid "CPU shares" +msgstr "Shares CPU" + +msgid "DNS options" +msgstr "Options DNS" + +msgid "DNS search" +msgstr "Recherche DNS" + +msgid "DNS servers" +msgstr "Serveurs DNS" + +msgid "Exit policy" +msgstr "Politique de sortie" + +msgid "Extra hosts" +msgstr "Hôtes supplémentaires" + +msgid "Host network" +msgstr "Réseau hôte" + +msgid "Infra container" +msgstr "Conteneur infra" + +msgid "Memory limit" +msgstr "Limite de mémoire" + +msgid "Net I/O" +msgstr "I/O réseau" + +msgid "Pause pod" +msgstr "Mettre le pod en pause" + +msgid "Pod is not running" +msgstr "Le pod n'est pas en cours d'exécution" + +msgid "Remove pod" +msgstr "Supprimer le pod" + +msgid "Restart policy" +msgstr "Politique de redémarrage" + +msgid "Restart pod" +msgstr "Redémarrer le pod" + +msgid "Shared namespaces" +msgstr "Namespaces partagés" + +msgid "Start pod" +msgstr "Démarrer le pod" + +msgid "Static IPv4" +msgstr "IPv4 statique" + +msgid "Static MAC" +msgstr "MAC statique" + +msgid "Stop pod" +msgstr "Arrêter le pod" + +msgid "Unlimited" +msgstr "Illimité" + +msgid "Unpause pod" +msgstr "Reprendre le pod" + +msgid "Stopping container" +msgstr "Arrêt du conteneur" + +msgid "Stop failed: %s" +msgstr "Échec de l'arrêt : %s" + +msgid "Removing container" +msgstr "Suppression du conteneur" + +msgid "Remove failed: %s" +msgstr "Échec de la suppression : %s" + +msgid "Creating container" +msgstr "Création du conteneur" + +msgid "Create failed: %s" +msgstr "Échec de la création : %s" + +msgid "Starting container" +msgstr "Démarrage du conteneur" + +msgid "Start failed: %s" +msgstr "Échec du démarrage : %s" + +msgid "Reinstating init script" +msgstr "Restauration du script d'initialisation" + +msgid "Init script reinstate failed: %s" +msgstr "Échec de la restauration du script d'initialisation : %s" + +msgid "Updated to %s" +msgstr "Mis à jour vers %s" + +msgid "Update image: %d/%d - %s" +msgstr "Mise à jour de l'image : %d/%d - %s" + +msgid "Removed old image %s" +msgstr "Ancienne image %s supprimée" + +msgid "Done." +msgstr "Terminé." + +msgid "Pause container" +msgstr "Mettre le conteneur en pause" + +msgid "Unpause container" +msgstr "Reprendre le conteneur" diff --git a/luci-app-podman/po/nl/podman.po b/luci-app-podman/po/nl/podman.po new file mode 100644 index 000000000..da95baac1 --- /dev/null +++ b/luci-app-podman/po/nl/podman.po @@ -0,0 +1,1090 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-05-03 11:05+0000\n" +"Last-Translator:\n" +"Language-Team: Dutch \n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "%d check(s) failed:" +msgstr "%d controle(s) mislukt:" + +msgid "%d updated, %d failed:" +msgstr "%d bijgewerkt, %d mislukt:" + +msgid "%s created successfully" +msgstr "%s succesvol aangemaakt" + +msgid "%s: %d/%d" +msgstr "%s: %d/%d" + +msgid "(none)" +msgstr "(geen)" + +msgid "-- Select %s --" +msgstr "-- Selecteer %s --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 tekens: alleen letters, cijfers, laag streepje (_) en koppelteken (-)" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m, 1g of -1 voor onbeperkt" + +msgid "API Version" +msgstr "API-versie" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "Container toevoegen aan een bestaande pod. Netwerk, hostnaam en poort-mappings komen van de infra-container van de pod." + +msgid "All images updated successfully" +msgstr "Alle images succesvol bijgewerkt" + +msgid "All volumes exported successfully" +msgstr "Alle volumes succesvol geëxporteerd" + +msgid "Allocate TTY (-t)" +msgstr "TTY toewijzen (-t)" + +msgid "Allocate container IP from this range" +msgstr "Container IP toewijzen uit dit bereik" + +msgid "Always" +msgstr "Altijd" + +msgid "Apply Updates" +msgstr "Updates toepassen" + +msgid "Are you sure to delete the records?" +msgstr "Records verwijderen?" + +msgid "Are you sure to remove container?" +msgstr "Container verwijderen?" + +msgid "Are you sure to remove integration?" +msgstr "Integratie verwijderen?" + +msgid "Auto Remove (--rm)" +msgstr "Automatisch verwijderen (--rm)" + +msgid "Auto Update" +msgstr "Automatisch bijwerken" + +msgid "Auto-Update" +msgstr "Automatisch bijwerken" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "Automatisch OpenWrt netwerkinterface, bridge en firewallzone configureren. Ten zeerste aanbevolen voor correct container-netwerkgebruik op OpenWrt." + +msgid "Automatically start the container after it is created" +msgstr "De container automatisch starten nadat deze is aangemaakt" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "Container automatisch bijwerken wanneer een nieuwer image beschikbaar is. Voegt label toe: io.containers.autoupdate=registry" + +msgid "Basic Information" +msgstr "Basis informatie" + +msgid "Block I/O" +msgstr "Blok I/O" + +msgid "Boot" +msgstr "Opstarten" + +msgid "Bridge Interface Name" +msgstr "Bridge interfacenaam" + +msgid "CPU" +msgstr "CPU" + +msgid "CPU Limit" +msgstr "CPU-limiet" + +msgid "CPU Set" +msgstr "CPU-set" + +msgid "CPU Shares Weight" +msgstr "CPU shares gewicht" + +msgid "CPU Usage" +msgstr "CPU-gebruik" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "CPU-aandelen (relatief gewicht). 0 = standaard." + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "CPU's waarop de pod mag draaien (bijv. \"0-3\" of \"0,2\")" + +msgid "Cancel" +msgstr "Annuleren" + +msgid "Check" +msgstr "Controleren" + +msgid "Check %d containers for updates?" +msgstr "%d containers controleren op updates?" + +msgid "Check Complete" +msgstr "Controle voltooid" + +msgid "Check for Updates" +msgstr "Controleren op updates" + +msgid "Check for updates: %s/%s" +msgstr "Controleren op updates: %s/%s" + +msgid "Choose firewall zone for this network." +msgstr "Firewallzone kiezen voor dit netwerk." + +msgid "Clean Up Now" +msgstr "Nu opruimen" + +msgid "Cleaning up old images..." +msgstr "Oude images opruimen..." + +msgid "Cleanup / Prune" +msgstr "Opruimen / Prune" + +msgid "Cleanup Complete" +msgstr "Opruimen voltooid" + +msgid "Cleanup Unused Resources" +msgstr "Ongebruikte bronnen opruimen" + +msgid "Cleanup successful!" +msgstr "Opruimen succesvol!" + +msgid "Clear logs" +msgstr "Logboeken wissen" + +msgid "Click icon to repair" +msgstr "Klik op het pictogram om te repareren" + +msgid "Click to disconnect" +msgstr "Klikken om te verbreken" + +msgid "Click to enable/disable" +msgstr "Klikken om in/uit te schakelen" + +msgid "Click to reveal/hide value" +msgstr "Klik om waarde te tonen/verbergen" + +msgid "Close" +msgstr "Sluiten" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "Door komma's gescheiden lijst van aanvullende groeps-ID's" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "Door komma's gescheiden poorten om te exposeren (bijv. 6052, 8080/udp)" + +msgid "Command" +msgstr "Opdracht" + +msgid "Command to run (space-separated)" +msgstr "Uit te voeren opdracht (spatie-gescheiden)" + +msgid "Configuration" +msgstr "Configuratie" + +msgid "Configure resource limits for this container." +msgstr "Resourcelimieten configureren voor deze container." + +msgid "Confirm" +msgstr "Bevestigen" + +msgid "Connect" +msgstr "Verbinden" + +msgid "Connect to" +msgstr "Verbinden met" + +msgid "Connected successfully to %s" +msgstr "Succesvol verbonden met %s" + +msgid "Connecting container to network" +msgstr "Container verbinden met netwerk" + +msgid "Console" +msgstr "Console" + +msgid "Container" +msgstr "Container" + +msgid "Container Name" +msgstr "Container naam" + +msgid "Container image has no tag - cannot update" +msgstr "Container-image heeft geen tag - kan niet bijwerken" + +msgid "Container image to use" +msgstr "Te gebruiken container image" + +msgid "Container is not running" +msgstr "Container is niet actief" + +msgid "Container updated successfully" +msgstr "Container succesvol bijgewerkt" + +msgid "Containers" +msgstr "Containers" + +msgid "Containers in pod %s" +msgstr "Containers in pod %s" + +msgid "Containers updated successfully" +msgstr "Containers succesvol bijgewerkt" + +msgid "Containers updated successfully." +msgstr "Containers succesvol bijgewerkt." + +msgid "Create" +msgstr "Aanmaken" + +msgid "Create %s" +msgstr "%s aanmaken" + +msgid "Create Command" +msgstr "Aanmaakcommando" + +msgid "Create Infra Container" +msgstr "Infra-container aanmaken" + +msgid "Create new zone (will be named: podman_)" +msgstr "Nieuwe zone aanmaken (naam wordt: podman_)" + +msgid "Created" +msgstr "Aangemaakt" + +msgid "Creating %s" +msgstr "Aanmaken van %s" + +msgid "Creating auto-start configuration for %s" +msgstr "Automatische start configuratie aanmaken voor %s" + +msgid "DNS Search Domains" +msgstr "DNS-zoekdomeinen" + +msgid "DNS Servers" +msgstr "DNS-servers" + +msgid "DNS search domains for the infra container" +msgstr "DNS-zoekdomeinen voor de infra-container" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "DNS-servers voor de infra-container (gedeeld met alle containers in de pod)" + +msgid "Delete" +msgstr "Verwijderen" + +msgid "Deleting records: %d/%d" +msgstr "Records verwijderen: %d/%d" + +msgid "Deleting records: %s/%s" +msgstr "Records verwijderen: %s/%s" + +msgid "Destination" +msgstr "Bestemming" + +msgid "Device" +msgstr "Apparaat" + +msgid "Disabled" +msgstr "Uitgeschakeld" + +msgid "Disabling init script" +msgstr "Init-script uitschakelen" + +msgid "Disconnect container from network" +msgstr "Container verbreken van netwerk" + +msgid "Disconnected from %s" +msgstr "Verbroken van %s" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "Driver" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "Driver-specifieke opties (komma-gescheiden, bijv. type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "IPv6 inschakelen" + +msgid "Enable IPv6 networking" +msgstr "IPv6-netwerkgebruik inschakelen" + +msgid "Enabling init script" +msgstr "Init-script inschakelen" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "Voer geheime gegevens in (wachtwoord, token, sleutel, etc.)" + +msgid "Entrypoint" +msgstr "Entrypoint" + +msgid "Environment Variables" +msgstr "Omgevingsvariabelen" + +msgid "Ephemeral (--rm flag)" +msgstr "Efemeer (--rm vlag)" + +msgid "Error" +msgstr "Fout" + +msgid "Error: %s" +msgstr "Fout: %s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "Bestaande fysieke interface om als parent te gebruiken. Vereist voor macvlan/ipvlan netwerken." + +msgid "Expecting: %s" +msgstr "Verwacht: %s" + +msgid "Export" +msgstr "Exporteren" + +msgid "Exporting selected volumes: %d/%d" +msgstr "Exporteren van geselecteerde volumes: %d/%d" + +msgid "Expose Ports" +msgstr "Poorten blootstellen" + +msgid "Failed to create container" +msgstr "Aanmaken van container mislukt" + +msgid "Fetching information..." +msgstr "Informatie ophalen..." + +msgid "Find updateable containers" +msgstr "Bijwerkbare containers zoeken" + +msgid "Firewall Zone" +msgstr "Firewallzone" + +msgid "Gateway" +msgstr "Gateway" + +msgid "Generate" +msgstr "Genereren" + +msgid "Graph Root" +msgstr "Grafiek-root" + +msgid "Health" +msgstr "Gezondheid" + +msgid "Hostname" +msgstr "Hostnaam" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Hostnaam voor de pod (standaard: pod-naam)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "IP-adres" + +msgid "IP Range (CIDR)" +msgstr "IP-bereik (CIDR)" + +msgid "IPv4 Gateway" +msgstr "IPv4 Gateway" + +msgid "IPv4 Subnet (CIDR)" +msgstr "IPv4 Subnet (CIDR)" + +msgid "IPv4 gateway address" +msgstr "IPv4 gateway-adres" + +msgid "IPv4 subnet in CIDR notation" +msgstr "IPv4-subnet in CIDR-notatie" + +msgid "Image" +msgstr "Image" + +msgid "Image ID" +msgstr "Image-ID" + +msgid "Image is already up-to-date" +msgstr "Image is al up-to-date" + +msgid "Image pulled successfully" +msgstr "Image succesvol opgehaald" + +msgid "Images" +msgstr "Images" + +msgid "Import" +msgstr "Importeren" + +msgid "Import from archive: %s" +msgstr "Importeren uit archief: %s" + +msgid "Importing volume..." +msgstr "Volume importeren..." + +msgid "Info" +msgstr "Info" + +msgid "Information" +msgstr "Informatie" + +msgid "Infra Image" +msgstr "Infra-image" + +msgid "Init Script" +msgstr "Init-script" + +msgid "Inspect" +msgstr "Inspecteren" + +msgid "Int" +msgstr "Geh" + +msgid "Interactive" +msgstr "Interactief" + +msgid "Interface" +msgstr "Interface" + +msgid "Internal Network" +msgstr "Intern netwerk" + +msgid "Labels" +msgstr "Labels" + +msgid "Labels in key=value format, one per line" +msgstr "Labels in sleutel=waarde formaat, één per regel" + +msgid "Leave empty to auto-generate" +msgstr "Leeg laten voor automatisch genereren" + +msgid "Links" +msgstr "Links" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Linux-namespaces gedeeld tussen de containers in de pod. Standaard komt overeen met `podman pod create`." + +msgid "Load" +msgstr "Laden" + +msgid "Load System diagnostics" +msgstr "Systeemdiagnostiek laden" + +msgid "Loading init script" +msgstr "Init-script laden" + +msgid "Loading..." +msgstr "Laden..." + +msgid "Logs" +msgstr "Logs" + +msgid "Manage podman %s" +msgstr "Podman %s beheren" + +msgid "Memory" +msgstr "Geheugen" + +msgid "Memory %" +msgstr "Geheugen %" + +msgid "Memory + Swap Limit" +msgstr "Geheugen + Swap limiet" + +msgid "Memory Limit" +msgstr "Geheugenlimiet" + +msgid "Memory Usage" +msgstr "Geheugengebruik" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "Geheugenlimiet (bijv. 512m, 1g)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "Geheugenlimiet (bijv. 512m, 1g). -1 = onbeperkt, 0 = standaard." + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Geheugenlimiet voor de pod (bijv. 512m, 1g)" + +msgid "Mode" +msgstr "Modus" + +msgid "Mount Options" +msgstr "Mount-opties" + +msgid "Mountpoint" +msgstr "Mountpoint" + +msgid "Mounts" +msgstr "Mounts" + +msgid "Must be between 0 and 262144" +msgstr "Moet tussen 0 en 262144 zijn" + +msgid "Name" +msgstr "Naam" + +msgid "Name for the network" +msgstr "Naam voor het netwerk" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "Naam van de bridge interface (bijv. podman0, mynet0). Leeg laten voor: <netwerknaam>0. Let op: Bij conflict met een bestaande interface zal OpenWrt dit automatisch ophogen." + +msgid "Name updated successfully" +msgstr "Naam succesvol bijgewerkt" + +msgid "Network" +msgstr "Netwerk" + +msgid "Network I/O" +msgstr "Netwerk I/O" + +msgid "Network Mode" +msgstr "Netwerkmodus" + +msgid "Network Name" +msgstr "Netwerknaam" + +msgid "Network driver" +msgstr "Netwerkdriver" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Netwerk-namespace voor de pod. Containers in de pod delen dit netwerk." + +msgid "Networks" +msgstr "Netwerken" + +msgid "Never" +msgstr "Nooit" + +msgid "No" +msgstr "Nee" + +msgid "No %s selected" +msgstr "Geen %s geselecteerd" + +msgid "No containers in this pod" +msgstr "Geen containers in deze pod" + +msgid "No containers with auto-update label found." +msgstr "Geen containers met auto-update label gevonden." + +msgid "No new updates available." +msgstr "Geen nieuwe updates beschikbaar." + +msgid "No suitable interfaces found" +msgstr "Geen geschikte interfaces gevonden" + +msgid "No unused resources found" +msgstr "Geen ongebruikte bronnen gevonden" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "Aantal CPU's (bijv. 0.5, 1.0, 2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "Aantal CPU's (bijv. 0.5, 1.0, 2.0). Leeg laten voor onbeperkt." + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Aantal CPU's beschikbaar voor de pod (bijv. 0.5, 1.0, 2.0)" + +msgid "Number of lines" +msgstr "Aantal regels" + +msgid "OK" +msgstr "OK" + +msgid "Old image still used by other containers" +msgstr "Oude image wordt nog gebruikt door andere containers" + +msgid "On Failure" +msgstr "Bij fout" + +msgid "One per line, format: host:container[/protocol]" +msgstr "Één per regel, formaat: host:container[/protocol]" + +msgid "One per line, format: key=value" +msgstr "Eén per regel, formaat: sleutel=waarde" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "Één per regel. Formaat: bron:bestemming[:opties]. Opties: ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "OpenWrt-integratie bestaat\n\nKlik om te verwijderen" + +msgid "OpenWrt integration incomplete" +msgstr "OpenWrt-integratie onvolledig" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "OpenWrt-integratie vereist opgave van een subnet" + +msgid "Override the image used for the infra container" +msgstr "Image overschrijven die voor de infra-container wordt gebruikt" + +msgid "PIDs" +msgstr "PIDs" + +msgid "Parent Interface" +msgstr "Parent-interface" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "Parent-interface is vereist voor macvlan/ipvlan netwerken" + +msgid "Part of a pod (\"%s\")" +msgstr "Onderdeel van pod \"%s\"" + +msgid "Pausing pod" +msgstr "Pod wordt gepauzeerd" + +msgid "Pod" +msgstr "Pod" + +msgid "Pod Name" +msgstr "Pod-naam" + +msgid "Podman Version" +msgstr "Podman-versie" + +msgid "Pods" +msgstr "Pods" + +msgid "Port Mappings" +msgstr "Poort-mappings" + +msgid "Ports" +msgstr "Poorten" + +msgid "Privileged" +msgstr "Privileged" + +msgid "Privileged Mode" +msgstr "Privileged-modus" + +msgid "Processes" +msgstr "Processen" + +msgid "Pull Image" +msgstr "Image ophalen" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "Pull heeft geen image-ID geretourneerd - pull is mogelijk mislukt" + +msgid "Pulling %s" +msgstr "%s ophalen" + +msgid "Read: %s / Write: %s" +msgstr "Lezen: %s / Schrijven: %s" + +msgid "Refreshing %s..." +msgstr "Vernieuwen van %s..." + +msgid "Registries" +msgstr "Registries" + +msgid "Registry" +msgstr "Registry" + +msgid "Reload" +msgstr "Herladen" + +msgid "Remove OpenWrt integration" +msgstr "OpenWrt-integratie verwijderen" + +msgid "Remove all unused images (not just dangling)" +msgstr "Alle ongebruikte images verwijderen (niet alleen dangling)" + +msgid "Remove container" +msgstr "Container verwijderen" + +msgid "Remove unused volumes" +msgstr "Ongebruikte volumes verwijderen" + +msgid "Removed: %s" +msgstr "Verwijderd: %s" + +msgid "Removing unused resources, please wait..." +msgstr "Ongebruikte bronnen verwijderen, even geduld..." + +msgid "Repository" +msgstr "Repository" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "Vereist voor gedeeld netwerk-namespace. Alleen uitschakelen als u weet dat dit niet nodig is." + +msgid "Resources" +msgstr "Bronnen" + +msgid "Restart Policy" +msgstr "Herstartbeleid" + +msgid "Restart container" +msgstr "Container opnieuw starten" + +msgid "Restart policy updated successfully" +msgstr "Herstartbeleid succesvol bijgewerkt" + +msgid "Restarting pod" +msgstr "Pod wordt herstart" + +msgid "Restrict external access to the network" +msgstr "Externe toegang tot het netwerk beperken" + +msgid "Run Root" +msgstr "Run-root" + +msgid "Saving resource" +msgstr "Resource opslaan" + +msgid "Secret" +msgstr "Geheim (Secret)" + +msgid "Secret Data" +msgstr "Geheime gegevens" + +msgid "Secret Name" +msgstr "Geheim naam" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "Geheim naam mag alleen letters, cijfers, underscores en koppeltekens bevatten" + +msgid "Secrets" +msgstr "Geheimen" + +msgid "Security Notice:" +msgstr "Veiligheidswaarschuwing:" + +msgid "Select containers to update" +msgstr "Containers selecteren om bij te werken" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "Netwerk selecteren voor de nieuwe container. Door gebruikers aangemaakte netwerken bieden betere isolatie en DNS-resolutie tussen containers." + +msgid "Select what to clean up:" +msgstr "Selecteer wat op te ruimen:" + +msgid "Sensitive data is hidden for security reasons." +msgstr "Gevoelige gegevens zijn verborgen om veiligheidsredenen." + +msgid "Setup OpenWrt Integration" +msgstr "OpenWrt-integratie instellen" + +msgid "Shared Namespaces" +msgstr "Gedeelde namespaces" + +msgid "Shares network with another container" +msgstr "Deelt netwerk met een andere container" + +msgid "Show" +msgstr "Tonen" + +msgid "Since" +msgstr "Vanaf" + +msgid "Size" +msgstr "Grootte" + +msgid "Socket Path" +msgstr "Socket-pad" + +msgid "Source" +msgstr "Bron" + +msgid "Space freed: %s" +msgstr "Ruimte vrijgemaakt: %s" + +msgid "Start after creation" +msgstr "Starten na aanmaak" + +msgid "Start container" +msgstr "Container starten" + +msgid "Start on boot" +msgstr "Starten bij opstarten" + +msgid "Start script disabled" +msgstr "Startscript uitgeschakeld" + +msgid "Start script missing\n\nClick to generate" +msgstr "Opstartscript ontbreekt\n\nKlik om te genereren" + +msgid "Start stream" +msgstr "Stream starten" + +msgid "Started" +msgstr "Gestart" + +msgid "Started At" +msgstr "Gestart om" + +msgid "Starting pod" +msgstr "Pod wordt gestart" + +msgid "Statistics" +msgstr "Statistieken" + +msgid "Stats" +msgstr "Statistieken" + +msgid "Status" +msgstr "Status" + +msgid "Stop container" +msgstr "Container stoppen" + +msgid "Stop stream" +msgstr "Stream stoppen" + +msgid "Stopping pod" +msgstr "Pod wordt gestopt" + +msgid "Subnet" +msgstr "Subnet" + +msgid "Supplementary Groups" +msgstr "Aanvullende groepen" + +msgid "System Diagnostics" +msgstr "Systeemdiagnose" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "Tag" + +msgid "The sensitive data to store securely" +msgstr "De gevoelige gegevens om veilig op te slaan" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "Voeg het label toe om automatisch bijwerken in te schakelen: io.containers.autoupdate=registry" + +msgid "Total" +msgstr "Totaal" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "Totale geheugenlimiet (geheugen + swap). -1 = onbeperkt, 0 = standaard." + +msgid "Type" +msgstr "Type" + +msgid "Unknown" +msgstr "Onbekend" + +msgid "Unless Stopped" +msgstr "Unless Stopped" + +msgid "Until" +msgstr "Tot" + +msgid "Update" +msgstr "Bijwerken" + +msgid "Update Resources" +msgstr "Bronnen bijwerken" + +msgid "Update name" +msgstr "Naam bijwerken" + +msgid "Update restart policy" +msgstr "Herstartbeleid bijwerken" + +msgid "Updating Images" +msgstr "Images bijwerken" + +msgid "Updating container" +msgstr "Container bijwerken" + +msgid "Upload failed: %s" +msgstr "Upload mislukt: %s" + +msgid "Uploading file..." +msgstr "Bestand uploaden..." + +msgid "User" +msgstr "Gebruiker" + +msgid "User and group to run as (UID:GID)" +msgstr "Gebruiker en groep (UID:GID)" + +msgid "Value" +msgstr "Waarde" + +msgid "Variable" +msgstr "Variabele" + +msgid "Volume" +msgstr "Volume" + +msgid "Volume Name" +msgstr "Volume naam" + +msgid "Volume driver to use" +msgstr "Te gebruiken volumedriver" + +msgid "Volume imported successfully" +msgstr "Volume succesvol geïmporteerd" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "Volumenaam. Leeg laten voor automatisch genereren." + +msgid "Volumes" +msgstr "Volumes" + +msgid "Working Directory" +msgstr "Werkmap" + +msgid "Yes" +msgstr "Ja" + +msgid "default" +msgstr "standaard" + +msgid "non-empty value" +msgstr "niet-lege waarde" + +msgid "running" +msgstr "actief" + +msgid "total" +msgstr "totaal" + +msgid "Are you sure to remove pod?" +msgstr "Weet u zeker dat u de pod wilt verwijderen?" + +msgid "Cgroup parent" +msgstr "Cgroup-parent" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "CPU %" + +msgid "CPU period" +msgstr "CPU-periode" + +msgid "CPU quota" +msgstr "CPU-quotum" + +msgid "CPU set" +msgstr "CPU-set" + +msgid "CPU shares" +msgstr "CPU-shares" + +msgid "DNS options" +msgstr "DNS-opties" + +msgid "DNS search" +msgstr "DNS-zoek" + +msgid "DNS servers" +msgstr "DNS-servers" + +msgid "Exit policy" +msgstr "Exit-beleid" + +msgid "Extra hosts" +msgstr "Extra hosts" + +msgid "Host network" +msgstr "Host-netwerk" + +msgid "Infra container" +msgstr "Infra-container" + +msgid "Memory limit" +msgstr "Geheugenlimiet" + +msgid "Net I/O" +msgstr "Netwerk-I/O" + +msgid "Pause pod" +msgstr "Pod pauzeren" + +msgid "Pod is not running" +msgstr "Pod is niet actief" + +msgid "Remove pod" +msgstr "Pod verwijderen" + +msgid "Restart policy" +msgstr "Herstartbeleid" + +msgid "Restart pod" +msgstr "Pod herstarten" + +msgid "Shared namespaces" +msgstr "Gedeelde namespaces" + +msgid "Start pod" +msgstr "Pod starten" + +msgid "Static IPv4" +msgstr "Statische IPv4" + +msgid "Static MAC" +msgstr "Statische MAC" + +msgid "Stop pod" +msgstr "Pod stoppen" + +msgid "Unlimited" +msgstr "Onbeperkt" + +msgid "Unpause pod" +msgstr "Pod hervatten" + +msgid "Stopping container" +msgstr "Container wordt gestopt" + +msgid "Stop failed: %s" +msgstr "Stoppen mislukt: %s" + +msgid "Removing container" +msgstr "Container wordt verwijderd" + +msgid "Remove failed: %s" +msgstr "Verwijderen mislukt: %s" + +msgid "Creating container" +msgstr "Container wordt aangemaakt" + +msgid "Create failed: %s" +msgstr "Aanmaken mislukt: %s" + +msgid "Starting container" +msgstr "Container wordt gestart" + +msgid "Start failed: %s" +msgstr "Starten mislukt: %s" + +msgid "Reinstating init script" +msgstr "Init-script wordt hersteld" + +msgid "Init script reinstate failed: %s" +msgstr "Herstellen van init-script mislukt: %s" + +msgid "Updated to %s" +msgstr "Bijgewerkt naar %s" + +msgid "Update image: %d/%d - %s" +msgstr "Image bijwerken: %d/%d - %s" + +msgid "Removed old image %s" +msgstr "Oude image %s verwijderd" + +msgid "Done." +msgstr "Klaar." + +msgid "Pause container" +msgstr "Container pauzeren" + +msgid "Unpause container" +msgstr "Container hervatten" diff --git a/luci-app-podman/po/ru/podman.po b/luci-app-podman/po/ru/podman.po new file mode 100644 index 000000000..af132e24d --- /dev/null +++ b/luci-app-podman/po/ru/podman.po @@ -0,0 +1,1093 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-05-03 11:05+0000\n" +"Last-Translator:\n" +"Language-Team: Russian \n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "" +"Start script missing\n" +"\n" +"Click to generate" +msgstr "Скрипт запуска отсутствует\n\nНажмите для создания" + +msgid "%d check(s) failed:" +msgstr "%d проверок не удалось:" + +msgid "%d updated, %d failed:" +msgstr "%d обновлено, %d не удалось:" + +msgid "%s created successfully" +msgstr "%s успешно создан" + +msgid "%s: %d/%d" +msgstr "%s: %d/%d" + +msgid "(none)" +msgstr "(нет)" + +msgid "-- Select %s --" +msgstr "-- Выберите %s --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 символа: только буквы, цифры, подчёркивание (_) и дефис (-)" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m, 1g или -1 для неограниченно" + +msgid "API Version" +msgstr "Версия API" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "Добавить контейнер в существующий под. Сеть, имя хоста и сопоставления портов берутся из infra-контейнера пода." + +msgid "All images updated successfully" +msgstr "Все образы успешно обновлены" + +msgid "All volumes exported successfully" +msgstr "Все тома успешно экспортированы" + +msgid "Allocate TTY (-t)" +msgstr "Выделить TTY (-t)" + +msgid "Allocate container IP from this range" +msgstr "Выделить IP контейнера из этого диапазона" + +msgid "Always" +msgstr "Всегда" + +msgid "Apply Updates" +msgstr "Применить обновления" + +msgid "Are you sure to delete the records?" +msgstr "Вы уверены, что хотите удалить записи?" + +msgid "Are you sure to remove container?" +msgstr "Вы уверены, что хотите удалить контейнер?" + +msgid "Are you sure to remove integration?" +msgstr "Вы уверены, что хотите удалить интеграцию?" + +msgid "Auto Remove (--rm)" +msgstr "Автоудаление (--rm)" + +msgid "Auto Update" +msgstr "Автообновление" + +msgid "Auto-Update" +msgstr "Автообновление" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "Автоматически настроить сетевой интерфейс OpenWrt, мост и зону брандмауэра. Настоятельно рекомендуется для правильной работы сети контейнеров на OpenWrt." + +msgid "Automatically start the container after it is created" +msgstr "Автоматически запускать контейнер после создания" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "Автоматически обновлять контейнер при наличии нового образа. Добавляет метку: `io.containers.autoupdate=registry`" + +msgid "Basic Information" +msgstr "Основная информация" + +msgid "Block I/O" +msgstr "Блочный ввод/вывод" + +msgid "Boot" +msgstr "Загрузка" + +msgid "Bridge Interface Name" +msgstr "Имя интерфейса моста" + +msgid "CPU" +msgstr "ЦП" + +msgid "CPU Limit" +msgstr "Ограничение ЦП" + +msgid "CPU Set" +msgstr "Набор CPU" + +msgid "CPU Shares Weight" +msgstr "Вес долей ЦП" + +msgid "CPU Usage" +msgstr "Использование ЦП" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "Доли ЦП (относительный вес). 0 = использовать по умолчанию." + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "CPU, на которых разрешено выполнять под (например, \"0-3\" или \"0,2\")" + +msgid "Cancel" +msgstr "Отмена" + +msgid "Check" +msgstr "Проверить" + +msgid "Check %d containers for updates?" +msgstr "Проверить %d контейнеров на обновления?" + +msgid "Check Complete" +msgstr "Проверка завершена" + +msgid "Check for Updates" +msgstr "Проверить обновления" + +msgid "Check for updates: %s/%s" +msgstr "Проверка обновлений: %s/%s" + +msgid "Choose firewall zone for this network." +msgstr "Выберите зону брандмауэра для этой сети." + +msgid "Clean Up Now" +msgstr "Очистить сейчас" + +msgid "Cleaning up old images..." +msgstr "Очистка старых образов..." + +msgid "Cleanup / Prune" +msgstr "Очистка / Удаление неиспользуемого" + +msgid "Cleanup Complete" +msgstr "Очистка завершена" + +msgid "Cleanup Unused Resources" +msgstr "Очистка неиспользуемых ресурсов" + +msgid "Cleanup successful!" +msgstr "Очистка успешна!" + +msgid "Clear logs" +msgstr "Очистить логи" + +msgid "Click icon to repair" +msgstr "Нажмите значок для восстановления" + +msgid "Click to disconnect" +msgstr "Нажмите для отключения" + +msgid "Click to enable/disable" +msgstr "Нажмите для включения/отключения" + +msgid "Click to reveal/hide value" +msgstr "Нажмите для отображения/скрытия значения" + +msgid "Close" +msgstr "Закрыть" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "Список дополнительных идентификаторов групп через запятую" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "Порты для публикации через запятую (например, 6052, 8080/udp)" + +msgid "Command" +msgstr "Команда" + +msgid "Command to run (space-separated)" +msgstr "Команда для запуска (разделённая пробелами)" + +msgid "Configuration" +msgstr "Конфигурация" + +msgid "Configure resource limits for this container." +msgstr "Настройте ограничения ресурсов для этого контейнера." + +msgid "Confirm" +msgstr "Подтвердить" + +msgid "Connect" +msgstr "Подключить" + +msgid "Connect to" +msgstr "Подключить к" + +msgid "Connected successfully to %s" +msgstr "Успешно подключено к %s" + +msgid "Connecting container to network" +msgstr "Подключение контейнера к сети" + +msgid "Console" +msgstr "Консоль" + +msgid "Container" +msgstr "Контейнер" + +msgid "Container Name" +msgstr "Имя контейнера" + +msgid "Container image has no tag - cannot update" +msgstr "Образ контейнера не имеет тега - невозможно обновить" + +msgid "Container image to use" +msgstr "Образ контейнера для использования" + +msgid "Container is not running" +msgstr "Контейнер не запущен" + +msgid "Container updated successfully" +msgstr "Контейнер успешно обновлён" + +msgid "Containers" +msgstr "Контейнеры" + +msgid "Containers in pod %s" +msgstr "Контейнеры в поде %s" + +msgid "Containers updated successfully" +msgstr "Контейнеры успешно обновлены" + +msgid "Containers updated successfully." +msgstr "Контейнеры успешно обновлены." + +msgid "Create" +msgstr "Создать" + +msgid "Create %s" +msgstr "Создать %s" + +msgid "Create Command" +msgstr "Команда создания" + +msgid "Create Infra Container" +msgstr "Создать infra-контейнер" + +msgid "Create new zone (will be named: podman_)" +msgstr "Создать новую зону (будет названа: podman_<имя-сети>)" + +msgid "Created" +msgstr "Создано" + +msgid "Creating %s" +msgstr "Создание %s" + +msgid "Creating auto-start configuration for %s" +msgstr "Создание конфигурации автозапуска для %s" + +msgid "DNS Search Domains" +msgstr "Домены поиска DNS" + +msgid "DNS Servers" +msgstr "DNS-серверы" + +msgid "DNS search domains for the infra container" +msgstr "Домены поиска DNS для infra-контейнера" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "DNS-серверы для infra-контейнера (общие для всех контейнеров пода)" + +msgid "Delete" +msgstr "Удалить" + +msgid "Deleting records: %d/%d" +msgstr "Удаление записей: %d/%d" + +msgid "Deleting records: %s/%s" +msgstr "Удаление записей: %s/%s" + +msgid "Destination" +msgstr "Назначение" + +msgid "Device" +msgstr "Устройство" + +msgid "Disabled" +msgstr "Отключено" + +msgid "Disabling init script" +msgstr "Отключение скрипта инициализации" + +msgid "Disconnect container from network" +msgstr "Отключить контейнер от сети" + +msgid "Disconnected from %s" +msgstr "Отключено от %s" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "Драйвер" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "Опции драйвера (разделённые запятыми, например, type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "Включить IPv6" + +msgid "Enable IPv6 networking" +msgstr "Включить сеть IPv6" + +msgid "Enabling init script" +msgstr "Включение скрипта инициализации" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "Введите секретные данные (пароль, токен, ключ и т.д.)" + +msgid "Entrypoint" +msgstr "Точка входа" + +msgid "Environment Variables" +msgstr "Переменные окружения" + +msgid "Ephemeral (--rm flag)" +msgstr "Эфемерный (флаг --rm)" + +msgid "Error" +msgstr "Ошибка" + +msgid "Error: %s" +msgstr "Ошибка: %s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "Существующий физический интерфейс для использования в качестве родительского. Требуется для сетей macvlan/ipvlan." + +msgid "Expecting: %s" +msgstr "Ожидается: %s" + +msgid "Export" +msgstr "Экспорт" + +msgid "Exporting selected volumes: %d/%d" +msgstr "Экспорт выбранных томов: %d/%d" + +msgid "Expose Ports" +msgstr "Открываемые порты" + +msgid "Failed to create container" +msgstr "Не удалось создать контейнер" + +msgid "Fetching information..." +msgstr "Получение информации..." + +msgid "Find updateable containers" +msgstr "Найти контейнеры с доступными обновлениями" + +msgid "Firewall Zone" +msgstr "Зона брандмауэра" + +msgid "Gateway" +msgstr "Шлюз" + +msgid "Generate" +msgstr "Сгенерировать" + +msgid "Graph Root" +msgstr "Корень графа" + +msgid "Health" +msgstr "Состояние" + +msgid "Hostname" +msgstr "Имя хоста" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Имя хоста для пода (по умолчанию — имя пода)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "IP-адрес" + +msgid "IP Range (CIDR)" +msgstr "Диапазон IP (CIDR)" + +msgid "IPv4 Gateway" +msgstr "Шлюз IPv4" + +msgid "IPv4 Subnet (CIDR)" +msgstr "Подсеть IPv4 (CIDR)" + +msgid "IPv4 gateway address" +msgstr "Адрес шлюза IPv4" + +msgid "IPv4 subnet in CIDR notation" +msgstr "Подсеть IPv4 в нотации CIDR" + +msgid "Image" +msgstr "Образ" + +msgid "Image ID" +msgstr "ID образа" + +msgid "Image is already up-to-date" +msgstr "Образ уже актуален" + +msgid "Image pulled successfully" +msgstr "Образ успешно загружен" + +msgid "Images" +msgstr "Образы" + +msgid "Import" +msgstr "Импорт" + +msgid "Import from archive: %s" +msgstr "Импорт из архива: %s" + +msgid "Importing volume..." +msgstr "Импорт тома..." + +msgid "Info" +msgstr "Инфо" + +msgid "Information" +msgstr "Информация" + +msgid "Infra Image" +msgstr "Infra-образ" + +msgid "Init Script" +msgstr "Скрипт инициализации" + +msgid "Inspect" +msgstr "Проверить" + +msgid "Int" +msgstr "Целое число" + +msgid "Interactive" +msgstr "Интерактивный" + +msgid "Interface" +msgstr "Интерфейс" + +msgid "Internal Network" +msgstr "Внутренняя сеть" + +msgid "Labels" +msgstr "Метки" + +msgid "Labels in key=value format, one per line" +msgstr "Метки в формате key=value, по одной на строку" + +msgid "Leave empty to auto-generate" +msgstr "Оставьте пустым для автогенерации" + +msgid "Links" +msgstr "Ссылки" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Пространства имён Linux, общие для контейнеров пода. По умолчанию соответствует `podman pod create`." + +msgid "Load" +msgstr "Загрузить" + +msgid "Load System diagnostics" +msgstr "Загрузить системную диагностику" + +msgid "Loading init script" +msgstr "Загрузка скрипта инициализации" + +msgid "Loading..." +msgstr "Загрузка..." + +msgid "Logs" +msgstr "Логи" + +msgid "Manage podman %s" +msgstr "Управление podman %s" + +msgid "Memory" +msgstr "Память" + +msgid "Memory %" +msgstr "Память %" + +msgid "Memory + Swap Limit" +msgstr "Память + Ограничение Swap" + +msgid "Memory Limit" +msgstr "Ограничение памяти" + +msgid "Memory Usage" +msgstr "Использование памяти" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "Ограничение памяти (например, 512м, 1г)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "Ограничение памяти (например, 512m, 1g). -1 = неограниченно, 0 = использовать по умолчанию." + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Лимит памяти для пода (например, 512m, 1g)" + +msgid "Mode" +msgstr "Режим" + +msgid "Mount Options" +msgstr "Опции монтирования" + +msgid "Mountpoint" +msgstr "Точка монтирования" + +msgid "Mounts" +msgstr "Монтирования" + +msgid "Must be between 0 and 262144" +msgstr "Должно быть между 0 и 262144" + +msgid "Name" +msgstr "Имя" + +msgid "Name for the network" +msgstr "Имя для сети" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "Имя интерфейса моста (например, podman0, mynet0). Оставьте пустым для использования: <имя-сети>0. Примечание: Если сгенерированное имя конфликтует с существующим интерфейсом, OpenWrt автоматически увеличит его." + +msgid "Name updated successfully" +msgstr "Имя успешно обновлено" + +msgid "Network" +msgstr "Сеть" + +msgid "Network I/O" +msgstr "Сетевой ввод/вывод" + +msgid "Network Mode" +msgstr "Сетевой режим" + +msgid "Network Name" +msgstr "Имя сети" + +msgid "Network driver" +msgstr "Сетевой драйвер" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Сетевое пространство имён для пода. Контейнеры в поде используют общую сеть." + +msgid "Networks" +msgstr "Сети" + +msgid "Never" +msgstr "Никогда" + +msgid "No" +msgstr "Нет" + +msgid "No %s selected" +msgstr "%s не выбрано" + +msgid "No containers in this pod" +msgstr "В этом поде нет контейнеров" + +msgid "No containers with auto-update label found." +msgstr "Контейнеры с меткой автообновления не найдены." + +msgid "No new updates available." +msgstr "Новые обновления недоступны." + +msgid "No suitable interfaces found" +msgstr "Подходящие интерфейсы не найдены" + +msgid "No unused resources found" +msgstr "Неиспользуемые ресурсы не найдены" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "Количество ЦП (например, 0.5, 1.0, 2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "Количество ЦП (например, 0.5, 1.0, 2.0). Оставьте пустым для неограниченно." + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Количество CPU, доступных поду (например, 0.5, 1.0, 2.0)" + +msgid "Number of lines" +msgstr "Количество строк" + +msgid "OK" +msgstr "OK" + +msgid "Old image still used by other containers" +msgstr "Старый образ ещё используется другими контейнерами" + +msgid "On Failure" +msgstr "При ошибке" + +msgid "One per line, format: host:container[/protocol]" +msgstr "По одному на строку, формат: хост:контейнер[/протокол]" + +msgid "One per line, format: key=value" +msgstr "По одному на строку, формат: key=value" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "По одному на строку. Формат: источник:назначение[:опции]. Опции: ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "Интеграция OpenWrt существует\n\nНажмите для удаления" + +msgid "OpenWrt integration incomplete" +msgstr "Интеграция с OpenWrt неполная" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "Интеграция с OpenWrt требует указания подсети" + +msgid "Override the image used for the infra container" +msgstr "Переопределить образ, используемый для infra-контейнера" + +msgid "PIDs" +msgstr "PID" + +msgid "Parent Interface" +msgstr "Родительский интерфейс" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "Родительский интерфейс требуется для сетей macvlan/ipvlan" + +msgid "Part of a pod (\"%s\")" +msgstr "Часть пода (\"%s\")" + +msgid "Pausing pod" +msgstr "Приостановка пода" + +msgid "Pod" +msgstr "Под" + +msgid "Pod Name" +msgstr "Имя пода" + +msgid "Podman Version" +msgstr "Версия Podman" + +msgid "Pods" +msgstr "Поды" + +msgid "Port Mappings" +msgstr "Маппинг портов" + +msgid "Ports" +msgstr "Порты" + +msgid "Privileged" +msgstr "Привилегированный" + +msgid "Privileged Mode" +msgstr "Привилегированный режим" + +msgid "Processes" +msgstr "Процессы" + +msgid "Pull Image" +msgstr "Загрузить образ" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "Загрузка не вернула ID образа - возможно, загрузка не удалась" + +msgid "Pulling %s" +msgstr "Загрузка %s" + +msgid "Read: %s / Write: %s" +msgstr "Чтение: %s / Запись: %s" + +msgid "Refreshing %s..." +msgstr "Обновление %s..." + +msgid "Registries" +msgstr "Реестры" + +msgid "Registry" +msgstr "Реестр" + +msgid "Reload" +msgstr "Перезагрузить" + +msgid "Remove OpenWrt integration" +msgstr "Удалить интеграцию с OpenWrt" + +msgid "Remove all unused images (not just dangling)" +msgstr "Удалить все неиспользуемые образы (не только висячие)" + +msgid "Remove container" +msgstr "Удалить контейнер" + +msgid "Remove unused volumes" +msgstr "Удалить неиспользуемые тома" + +msgid "Removed: %s" +msgstr "Удалено: %s" + +msgid "Removing unused resources, please wait..." +msgstr "Удаление неиспользуемых ресурсов, пожалуйста, подождите..." + +msgid "Repository" +msgstr "Репозиторий" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "Требуется для общего сетевого пространства имён. Отключайте только если уверены, что это не нужно." + +msgid "Resources" +msgstr "Ресурсы" + +msgid "Restart Policy" +msgstr "Политика перезапуска" + +msgid "Restart container" +msgstr "Перезапустить контейнер" + +msgid "Restart policy updated successfully" +msgstr "Политика перезапуска успешно обновлена" + +msgid "Restarting pod" +msgstr "Перезапуск пода" + +msgid "Restrict external access to the network" +msgstr "Ограничить внешний доступ к сети" + +msgid "Run Root" +msgstr "Корень запуска" + +msgid "Saving resource" +msgstr "Сохранение ресурса" + +msgid "Secret" +msgstr "Секрет" + +msgid "Secret Data" +msgstr "Секретные данные" + +msgid "Secret Name" +msgstr "Имя секрета" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "Имя секрета может содержать только буквы, цифры, подчёркивания и дефисы" + +msgid "Secrets" +msgstr "Секреты" + +msgid "Security Notice:" +msgstr "Уведомление о безопасности:" + +msgid "Select containers to update" +msgstr "Выберите контейнеры для обновления" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "Выберите сеть для нового контейнера. Пользовательские сети обеспечивают лучшую изоляцию и разрешение DNS между контейнерами." + +msgid "Select what to clean up:" +msgstr "Выберите, что очистить:" + +msgid "Sensitive data is hidden for security reasons." +msgstr "Конфиденциальные данные скрыты по соображениям безопасности." + +msgid "Setup OpenWrt Integration" +msgstr "Настроить интеграцию с OpenWrt" + +msgid "Shared Namespaces" +msgstr "Общие пространства имён" + +msgid "Shares network with another container" +msgstr "Использует общую сеть с другим контейнером" + +msgid "Show" +msgstr "Показать" + +msgid "Since" +msgstr "С" + +msgid "Size" +msgstr "Размер" + +msgid "Socket Path" +msgstr "Путь к сокету" + +msgid "Source" +msgstr "Источник" + +msgid "Space freed: %s" +msgstr "Освобождено места: %s" + +msgid "Start after creation" +msgstr "Запустить после создания" + +msgid "Start container" +msgstr "Запустить контейнер" + +msgid "Start on boot" +msgstr "Запускать при загрузке" + +msgid "Start script disabled" +msgstr "Скрипт запуска отключён" + +msgid "Start stream" +msgstr "Начать поток" + +msgid "Started" +msgstr "Запущено" + +msgid "Started At" +msgstr "Запущено в" + +msgid "Starting pod" +msgstr "Запуск пода" + +msgid "Statistics" +msgstr "Статистика" + +msgid "Stats" +msgstr "Статистика" + +msgid "Status" +msgstr "Статус" + +msgid "Stop container" +msgstr "Остановить контейнер" + +msgid "Stop stream" +msgstr "Остановить поток" + +msgid "Stopping pod" +msgstr "Остановка пода" + +msgid "Subnet" +msgstr "Подсеть" + +msgid "Supplementary Groups" +msgstr "Дополнительные группы" + +msgid "System Diagnostics" +msgstr "Системная диагностика" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "Тег" + +msgid "The sensitive data to store securely" +msgstr "Конфиденциальные данные для безопасного хранения" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "Для включения автообновления контейнера добавьте метку: `io.containers.autoupdate=registry`" + +msgid "Total" +msgstr "Всего" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "Общее ограничение памяти (память + swap). -1 = неограниченно, 0 = использовать по умолчанию." + +msgid "Type" +msgstr "Тип" + +msgid "Unknown" +msgstr "Неизвестно" + +msgid "Unless Stopped" +msgstr "Пока не остановлен" + +msgid "Until" +msgstr "До" + +msgid "Update" +msgstr "Обновить" + +msgid "Update Resources" +msgstr "Обновить ресурсы" + +msgid "Update name" +msgstr "Обновить имя" + +msgid "Update restart policy" +msgstr "Обновить политику перезапуска" + +msgid "Updating Images" +msgstr "Обновление образов" + +msgid "Updating container" +msgstr "Обновление контейнера" + +msgid "Upload failed: %s" +msgstr "Загрузка не удалась: %s" + +msgid "Uploading file..." +msgstr "Загрузка файла..." + +msgid "User" +msgstr "Пользователь" + +msgid "User and group to run as (UID:GID)" +msgstr "Пользователь и группа для запуска (UID:GID)" + +msgid "Value" +msgstr "Значение" + +msgid "Variable" +msgstr "Переменная" + +msgid "Volume" +msgstr "Том" + +msgid "Volume Name" +msgstr "Имя тома" + +msgid "Volume driver to use" +msgstr "Драйвер тома для использования" + +msgid "Volume imported successfully" +msgstr "Том успешно импортирован" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "Имя тома. Оставьте пустым для автогенерации." + +msgid "Volumes" +msgstr "Тома" + +msgid "Working Directory" +msgstr "Рабочая директория" + +msgid "Yes" +msgstr "Да" + +msgid "default" +msgstr "по умолчанию" + +msgid "non-empty value" +msgstr "непустое значение" + +msgid "running" +msgstr "запущен" + +msgid "total" +msgstr "всего" + +msgid "Are you sure to remove pod?" +msgstr "Действительно удалить под?" + +msgid "Cgroup parent" +msgstr "Родитель cgroup" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "ЦП %" + +msgid "CPU period" +msgstr "Период CPU" + +msgid "CPU quota" +msgstr "Квота CPU" + +msgid "CPU set" +msgstr "Набор CPU" + +msgid "CPU shares" +msgstr "Доли CPU" + +msgid "DNS options" +msgstr "Параметры DNS" + +msgid "DNS search" +msgstr "Поиск DNS" + +msgid "DNS servers" +msgstr "DNS-серверы" + +msgid "Exit policy" +msgstr "Политика выхода" + +msgid "Extra hosts" +msgstr "Дополнительные хосты" + +msgid "Host network" +msgstr "Сеть хоста" + +msgid "Infra container" +msgstr "Infra-контейнер" + +msgid "Memory limit" +msgstr "Лимит памяти" + +msgid "Net I/O" +msgstr "Сетевой I/O" + +msgid "Pause pod" +msgstr "Приостановить под" + +msgid "Pod is not running" +msgstr "Под не запущен" + +msgid "Remove pod" +msgstr "Удалить под" + +msgid "Restart policy" +msgstr "Политика перезапуска" + +msgid "Restart pod" +msgstr "Перезапустить под" + +msgid "Shared namespaces" +msgstr "Общие пространства имён" + +msgid "Start pod" +msgstr "Запустить под" + +msgid "Static IPv4" +msgstr "Статический IPv4" + +msgid "Static MAC" +msgstr "Статический MAC" + +msgid "Stop pod" +msgstr "Остановить под" + +msgid "Unlimited" +msgstr "Без ограничений" + +msgid "Unpause pod" +msgstr "Возобновить под" + +msgid "Stopping container" +msgstr "Остановка контейнера" + +msgid "Stop failed: %s" +msgstr "Ошибка остановки: %s" + +msgid "Removing container" +msgstr "Удаление контейнера" + +msgid "Remove failed: %s" +msgstr "Ошибка удаления: %s" + +msgid "Creating container" +msgstr "Создание контейнера" + +msgid "Create failed: %s" +msgstr "Ошибка создания: %s" + +msgid "Starting container" +msgstr "Запуск контейнера" + +msgid "Start failed: %s" +msgstr "Ошибка запуска: %s" + +msgid "Reinstating init script" +msgstr "Восстановление скрипта инициализации" + +msgid "Init script reinstate failed: %s" +msgstr "Ошибка восстановления скрипта инициализации: %s" + +msgid "Updated to %s" +msgstr "Обновлено до %s" + +msgid "Update image: %d/%d - %s" +msgstr "Обновление образа: %d/%d - %s" + +msgid "Removed old image %s" +msgstr "Старый образ %s удалён" + +msgid "Done." +msgstr "Готово." + +msgid "Pause container" +msgstr "Приостановить контейнер" + +msgid "Unpause container" +msgstr "Возобновить контейнер" diff --git a/luci-app-podman/po/templates/podman.pot b/luci-app-podman/po/templates/podman.pot new file mode 100644 index 000000000..2641b17eb --- /dev/null +++ b/luci-app-podman/po/templates/podman.pot @@ -0,0 +1,1081 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +msgid "%d check(s) failed:" +msgstr "" + +msgid "%d updated, %d failed:" +msgstr "" + +msgid "%s created successfully" +msgstr "" + +msgid "%s: %d/%d" +msgstr "" + +msgid "(none)" +msgstr "" + +msgid "-- Select %s --" +msgstr "" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "" + +msgid "API Version" +msgstr "" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "" + +msgid "All images updated successfully" +msgstr "" + +msgid "All volumes exported successfully" +msgstr "" + +msgid "Allocate TTY (-t)" +msgstr "" + +msgid "Allocate container IP from this range" +msgstr "" + +msgid "Always" +msgstr "" + +msgid "Apply Updates" +msgstr "" + +msgid "Are you sure to delete the records?" +msgstr "" + +msgid "Are you sure to remove container?" +msgstr "" + +msgid "Are you sure to remove integration?" +msgstr "" + +msgid "Auto Remove (--rm)" +msgstr "" + +msgid "Auto Update" +msgstr "" + +msgid "Auto-Update" +msgstr "" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "" + +msgid "Automatically start the container after it is created" +msgstr "" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "" + +msgid "Basic Information" +msgstr "" + +msgid "Block I/O" +msgstr "" + +msgid "Boot" +msgstr "" + +msgid "Bridge Interface Name" +msgstr "" + +msgid "CPU" +msgstr "" + +msgid "CPU Limit" +msgstr "" + +msgid "CPU Set" +msgstr "" + +msgid "CPU Shares Weight" +msgstr "" + +msgid "CPU Usage" +msgstr "" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "" + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Check" +msgstr "" + +msgid "Check %d containers for updates?" +msgstr "" + +msgid "Check Complete" +msgstr "" + +msgid "Check for Updates" +msgstr "" + +msgid "Check for updates: %s/%s" +msgstr "" + +msgid "Choose firewall zone for this network." +msgstr "" + +msgid "Clean Up Now" +msgstr "" + +msgid "Cleaning up old images..." +msgstr "" + +msgid "Cleanup / Prune" +msgstr "" + +msgid "Cleanup Complete" +msgstr "" + +msgid "Cleanup Unused Resources" +msgstr "" + +msgid "Cleanup successful!" +msgstr "" + +msgid "Clear logs" +msgstr "" + +msgid "Click icon to repair" +msgstr "" + +msgid "Click to disconnect" +msgstr "" + +msgid "Click to enable/disable" +msgstr "" + +msgid "Click to reveal/hide value" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "" + +msgid "Command" +msgstr "" + +msgid "Command to run (space-separated)" +msgstr "" + +msgid "Configuration" +msgstr "" + +msgid "Configure resource limits for this container." +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Connect" +msgstr "" + +msgid "Connect to" +msgstr "" + +msgid "Connected successfully to %s" +msgstr "" + +msgid "Connecting container to network" +msgstr "" + +msgid "Console" +msgstr "" + +msgid "Container" +msgstr "" + +msgid "Container Name" +msgstr "" + +msgid "Container image has no tag - cannot update" +msgstr "" + +msgid "Container image to use" +msgstr "" + +msgid "Container is not running" +msgstr "" + +msgid "Container updated successfully" +msgstr "" + +msgid "Containers" +msgstr "" + +msgid "Containers in pod %s" +msgstr "" + +msgid "Containers updated successfully" +msgstr "" + +msgid "Containers updated successfully." +msgstr "" + +msgid "Create" +msgstr "" + +msgid "Create %s" +msgstr "" + +msgid "Create Command" +msgstr "" + +msgid "Create Infra Container" +msgstr "" + +msgid "Create new zone (will be named: podman_)" +msgstr "" + +msgid "Created" +msgstr "" + +msgid "Creating %s" +msgstr "" + +msgid "Creating auto-start configuration for %s" +msgstr "" + +msgid "DNS Search Domains" +msgstr "" + +msgid "DNS Servers" +msgstr "" + +msgid "DNS search domains for the infra container" +msgstr "" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "" + +msgid "Delete" +msgstr "" + +msgid "Deleting records: %d/%d" +msgstr "" + +msgid "Deleting records: %s/%s" +msgstr "" + +msgid "Destination" +msgstr "" + +msgid "Device" +msgstr "" + +msgid "Disabled" +msgstr "" + +msgid "Disabling init script" +msgstr "" + +msgid "Disconnect container from network" +msgstr "" + +msgid "Disconnected from %s" +msgstr "" + +msgid "Dnsmasq" +msgstr "" + +msgid "Driver" +msgstr "" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "" + +msgid "Enable IPv6" +msgstr "" + +msgid "Enable IPv6 networking" +msgstr "" + +msgid "Enabling init script" +msgstr "" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "" + +msgid "Entrypoint" +msgstr "" + +msgid "Environment Variables" +msgstr "" + +msgid "Ephemeral (--rm flag)" +msgstr "" + +msgid "Error" +msgstr "" + +msgid "Error: %s" +msgstr "" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "" + +msgid "Expecting: %s" +msgstr "" + +msgid "Export" +msgstr "" + +msgid "Exporting selected volumes: %d/%d" +msgstr "" + +msgid "Expose Ports" +msgstr "" + +msgid "Failed to create container" +msgstr "" + +msgid "Fetching information..." +msgstr "" + +msgid "Find updateable containers" +msgstr "" + +msgid "Firewall Zone" +msgstr "" + +msgid "Gateway" +msgstr "" + +msgid "Generate" +msgstr "" + +msgid "Graph Root" +msgstr "" + +msgid "Health" +msgstr "" + +msgid "Hostname" +msgstr "" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "" + +msgid "ID" +msgstr "" + +msgid "IP Address" +msgstr "" + +msgid "IP Range (CIDR)" +msgstr "" + +msgid "IPv4 Gateway" +msgstr "" + +msgid "IPv4 Subnet (CIDR)" +msgstr "" + +msgid "IPv4 gateway address" +msgstr "" + +msgid "IPv4 subnet in CIDR notation" +msgstr "" + +msgid "Image" +msgstr "" + +msgid "Image ID" +msgstr "" + +msgid "Image is already up-to-date" +msgstr "" + +msgid "Image pulled successfully" +msgstr "" + +msgid "Images" +msgstr "" + +msgid "Import" +msgstr "" + +msgid "Import from archive: %s" +msgstr "" + +msgid "Importing volume..." +msgstr "" + +msgid "Info" +msgstr "" + +msgid "Information" +msgstr "" + +msgid "Infra Image" +msgstr "" + +msgid "Init Script" +msgstr "" + +msgid "Inspect" +msgstr "" + +msgid "Int" +msgstr "" + +msgid "Interactive" +msgstr "" + +msgid "Interface" +msgstr "" + +msgid "Internal Network" +msgstr "" + +msgid "Labels" +msgstr "" + +msgid "Labels in key=value format, one per line" +msgstr "" + +msgid "Leave empty to auto-generate" +msgstr "" + +msgid "Links" +msgstr "" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "" + +msgid "Load" +msgstr "" + +msgid "Load System diagnostics" +msgstr "" + +msgid "Loading init script" +msgstr "" + +msgid "Loading..." +msgstr "" + +msgid "Logs" +msgstr "" + +msgid "Manage podman %s" +msgstr "" + +msgid "Memory" +msgstr "" + +msgid "Memory %" +msgstr "" + +msgid "Memory + Swap Limit" +msgstr "" + +msgid "Memory Limit" +msgstr "" + +msgid "Memory Usage" +msgstr "" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "" + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "" + +msgid "Mode" +msgstr "" + +msgid "Mount Options" +msgstr "" + +msgid "Mountpoint" +msgstr "" + +msgid "Mounts" +msgstr "" + +msgid "Must be between 0 and 262144" +msgstr "" + +msgid "Name" +msgstr "" + +msgid "Name for the network" +msgstr "" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "" + +msgid "Name updated successfully" +msgstr "" + +msgid "Network" +msgstr "" + +msgid "Network I/O" +msgstr "" + +msgid "Network Mode" +msgstr "" + +msgid "Network Name" +msgstr "" + +msgid "Network driver" +msgstr "" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "" + +msgid "Networks" +msgstr "" + +msgid "Never" +msgstr "" + +msgid "No" +msgstr "" + +msgid "No %s selected" +msgstr "" + +msgid "No containers in this pod" +msgstr "" + +msgid "No containers with auto-update label found." +msgstr "" + +msgid "No new updates available." +msgstr "" + +msgid "No suitable interfaces found" +msgstr "" + +msgid "No unused resources found" +msgstr "" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "" + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "" + +msgid "Number of lines" +msgstr "" + +msgid "OK" +msgstr "" + +msgid "Old image still used by other containers" +msgstr "" + +msgid "On Failure" +msgstr "" + +msgid "One per line, format: host:container[/protocol]" +msgstr "" + +msgid "One per line, format: key=value" +msgstr "" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "" + +msgid "OpenWrt integration incomplete" +msgstr "" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "" + +msgid "Override the image used for the infra container" +msgstr "" + +msgid "PIDs" +msgstr "" + +msgid "Parent Interface" +msgstr "" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "" + +msgid "Part of a pod (\"%s\")" +msgstr "" + +msgid "Pausing pod" +msgstr "" + +msgid "Pod" +msgstr "" + +msgid "Pod Name" +msgstr "" + +msgid "Podman Version" +msgstr "" + +msgid "Pods" +msgstr "" + +msgid "Port Mappings" +msgstr "" + +msgid "Ports" +msgstr "" + +msgid "Privileged" +msgstr "" + +msgid "Privileged Mode" +msgstr "" + +msgid "Processes" +msgstr "" + +msgid "Pull Image" +msgstr "" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "" + +msgid "Pulling %s" +msgstr "" + +msgid "Read: %s / Write: %s" +msgstr "" + +msgid "Refreshing %s..." +msgstr "" + +msgid "Registries" +msgstr "" + +msgid "Registry" +msgstr "" + +msgid "Reload" +msgstr "" + +msgid "Remove OpenWrt integration" +msgstr "" + +msgid "Remove all unused images (not just dangling)" +msgstr "" + +msgid "Remove container" +msgstr "" + +msgid "Remove unused volumes" +msgstr "" + +msgid "Removed: %s" +msgstr "" + +msgid "Removing unused resources, please wait..." +msgstr "" + +msgid "Repository" +msgstr "" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "" + +msgid "Resources" +msgstr "" + +msgid "Restart Policy" +msgstr "" + +msgid "Restart container" +msgstr "" + +msgid "Restart policy updated successfully" +msgstr "" + +msgid "Restarting pod" +msgstr "" + +msgid "Restrict external access to the network" +msgstr "" + +msgid "Run Root" +msgstr "" + +msgid "Saving resource" +msgstr "" + +msgid "Secret" +msgstr "" + +msgid "Secret Data" +msgstr "" + +msgid "Secret Name" +msgstr "" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "" + +msgid "Secrets" +msgstr "" + +msgid "Security Notice:" +msgstr "" + +msgid "Select containers to update" +msgstr "" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "" + +msgid "Select what to clean up:" +msgstr "" + +msgid "Sensitive data is hidden for security reasons." +msgstr "" + +msgid "Setup OpenWrt Integration" +msgstr "" + +msgid "Shared Namespaces" +msgstr "" + +msgid "Shares network with another container" +msgstr "" + +msgid "Show" +msgstr "" + +msgid "Since" +msgstr "" + +msgid "Size" +msgstr "" + +msgid "Socket Path" +msgstr "" + +msgid "Source" +msgstr "" + +msgid "Space freed: %s" +msgstr "" + +msgid "Start after creation" +msgstr "" + +msgid "Start container" +msgstr "" + +msgid "Start on boot" +msgstr "" + +msgid "Start script disabled" +msgstr "" + +msgid "Start script missing\n\nClick to generate" +msgstr "" + +msgid "Start stream" +msgstr "" + +msgid "Started" +msgstr "" + +msgid "Started At" +msgstr "" + +msgid "Starting pod" +msgstr "" + +msgid "Statistics" +msgstr "" + +msgid "Stats" +msgstr "" + +msgid "Status" +msgstr "" + +msgid "Stop container" +msgstr "" + +msgid "Stop stream" +msgstr "" + +msgid "Stopping pod" +msgstr "" + +msgid "Subnet" +msgstr "" + +msgid "Supplementary Groups" +msgstr "" + +msgid "System Diagnostics" +msgstr "" + +msgid "TTY" +msgstr "" + +msgid "Tag" +msgstr "" + +msgid "The sensitive data to store securely" +msgstr "" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "" + +msgid "Total" +msgstr "" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "" + +msgid "Type" +msgstr "" + +msgid "Unknown" +msgstr "" + +msgid "Unless Stopped" +msgstr "" + +msgid "Until" +msgstr "" + +msgid "Update" +msgstr "" + +msgid "Update Resources" +msgstr "" + +msgid "Update name" +msgstr "" + +msgid "Update restart policy" +msgstr "" + +msgid "Updating Images" +msgstr "" + +msgid "Updating container" +msgstr "" + +msgid "Upload failed: %s" +msgstr "" + +msgid "Uploading file..." +msgstr "" + +msgid "User" +msgstr "" + +msgid "User and group to run as (UID:GID)" +msgstr "" + +msgid "Value" +msgstr "" + +msgid "Variable" +msgstr "" + +msgid "Volume" +msgstr "" + +msgid "Volume Name" +msgstr "" + +msgid "Volume driver to use" +msgstr "" + +msgid "Volume imported successfully" +msgstr "" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "" + +msgid "Volumes" +msgstr "" + +msgid "Working Directory" +msgstr "" + +msgid "Yes" +msgstr "" + +msgid "default" +msgstr "" + +msgid "non-empty value" +msgstr "" + +msgid "running" +msgstr "" + +msgid "total" +msgstr "" +msgid "Are you sure to remove pod?" +msgstr "" + +msgid "Cgroup parent" +msgstr "" + +msgid "CID" +msgstr "" + +msgid "CPU %" +msgstr "" + +msgid "CPU period" +msgstr "" + +msgid "CPU quota" +msgstr "" + +msgid "CPU set" +msgstr "" + +msgid "CPU shares" +msgstr "" + +msgid "DNS options" +msgstr "" + +msgid "DNS search" +msgstr "" + +msgid "DNS servers" +msgstr "" + +msgid "Exit policy" +msgstr "" + +msgid "Extra hosts" +msgstr "" + +msgid "Host network" +msgstr "" + +msgid "Infra container" +msgstr "" + +msgid "Memory limit" +msgstr "" + +msgid "Net I/O" +msgstr "" + +msgid "Pause pod" +msgstr "" + +msgid "Pod is not running" +msgstr "" + +msgid "Remove pod" +msgstr "" + +msgid "Restart policy" +msgstr "" + +msgid "Restart pod" +msgstr "" + +msgid "Shared namespaces" +msgstr "" + +msgid "Start pod" +msgstr "" + +msgid "Static IPv4" +msgstr "" + +msgid "Static MAC" +msgstr "" + +msgid "Stop pod" +msgstr "" + +msgid "Unlimited" +msgstr "" + +msgid "Unpause pod" +msgstr "" + +msgid "Stopping container" +msgstr "" + +msgid "Stop failed: %s" +msgstr "" + +msgid "Removing container" +msgstr "" + +msgid "Remove failed: %s" +msgstr "" + +msgid "Creating container" +msgstr "" + +msgid "Create failed: %s" +msgstr "" + +msgid "Starting container" +msgstr "" + +msgid "Start failed: %s" +msgstr "" + +msgid "Reinstating init script" +msgstr "" + +msgid "Init script reinstate failed: %s" +msgstr "" + +msgid "Updated to %s" +msgstr "" + +msgid "Update image: %d/%d - %s" +msgstr "" + +msgid "Removed old image %s" +msgstr "" + +msgid "Done." +msgstr "" + +msgid "Pause container" +msgstr "" + +msgid "Unpause container" +msgstr "" diff --git a/luci-app-podman/po/zh-cn b/luci-app-podman/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-podman/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-podman/po/zh_Hans/podman.po b/luci-app-podman/po/zh_Hans/podman.po new file mode 100644 index 000000000..ee7814ee5 --- /dev/null +++ b/luci-app-podman/po/zh_Hans/podman.po @@ -0,0 +1,1090 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-06-03 05:20+0000\n" +"Last-Translator:\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" + +msgid "%d check(s) failed:" +msgstr "%d 次检查失败:" + +msgid "%d updated, %d failed:" +msgstr "%d 已更新,%d 失败:" + +msgid "%s created successfully" +msgstr "%s 创建成功" + +msgid "%s: %d/%d" +msgstr "%s:%d/%d" + +msgid "(none)" +msgstr "(无)" + +msgid "-- Select %s --" +msgstr "-- 选择 %s --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 个字符:仅限字母、数字、下划线 (_) 和连字符 (-)" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m、1g 或 -1(无限制)" + +msgid "API Version" +msgstr "API 版本" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "将容器添加到现有 Pod。网络、主机名和端口映射来自 Pod 的 infra 容器。" + +msgid "All images updated successfully" +msgstr "所有镜像更新成功" + +msgid "All volumes exported successfully" +msgstr "所有卷已成功导出" + +msgid "Allocate TTY (-t)" +msgstr "分配 TTY (-t)" + +msgid "Allocate container IP from this range" +msgstr "从此范围分配容器 IP" + +msgid "Always" +msgstr "始终" + +msgid "Apply Updates" +msgstr "应用更新" + +msgid "Are you sure to delete the records?" +msgstr "确定删除这些记录吗?" + +msgid "Are you sure to remove container?" +msgstr "确定移除此容器吗?" + +msgid "Are you sure to remove integration?" +msgstr "确定移除此集成吗?" + +msgid "Auto Remove (--rm)" +msgstr "自动删除 (--rm)" + +msgid "Auto Update" +msgstr "自动更新" + +msgid "Auto-Update" +msgstr "自动更新" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "自动配置 OpenWrt 网络接口、桥接和防火墙区域。强烈建议勾选以在 OpenWrt 上进行正确的容器网络设置。" + +msgid "Automatically start the container after it is created" +msgstr "容器创建后自动启动" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "当有新镜像可用时自动更新容器。添加标签:io.containers.autoupdate=registry" + +msgid "Basic Information" +msgstr "基本信息" + +msgid "Block I/O" +msgstr "块 I/O" + +msgid "Boot" +msgstr "开机自启" + +msgid "Bridge Interface Name" +msgstr "网桥接口名称" + +msgid "CPU" +msgstr "CPU" + +msgid "CPU Limit" +msgstr "CPU 限制" + +msgid "CPU Set" +msgstr "CPU 集合" + +msgid "CPU Shares Weight" +msgstr "CPU 份额权重" + +msgid "CPU Usage" +msgstr "CPU 使用率" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "CPU 份额(相对权重)。0 = 使用默认值。" + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "允许 Pod 运行的 CPU(例如 \"0-3\" 或 \"0,2\")" + +msgid "Cancel" +msgstr "取消" + +msgid "Check" +msgstr "检查" + +msgid "Check %d containers for updates?" +msgstr "检查 %d 个容器的更新?" + +msgid "Check Complete" +msgstr "检查完成" + +msgid "Check for Updates" +msgstr "检查更新" + +msgid "Check for updates: %s/%s" +msgstr "检查更新:%s/%s" + +msgid "Choose firewall zone for this network." +msgstr "为此网络选择防火墙区域。" + +msgid "Clean Up Now" +msgstr "立即清理" + +msgid "Cleaning up old images..." +msgstr "正在清理旧镜像..." + +msgid "Cleanup / Prune" +msgstr "清理 / 修剪" + +msgid "Cleanup Complete" +msgstr "清理完成" + +msgid "Cleanup Unused Resources" +msgstr "清理未使用的资源" + +msgid "Cleanup successful!" +msgstr "清理成功!" + +msgid "Clear logs" +msgstr "清除日志" + +msgid "Click icon to repair" +msgstr "点击图标进行修复" + +msgid "Click to disconnect" +msgstr "点击断开连接" + +msgid "Click to enable/disable" +msgstr "点击启用/禁用" + +msgid "Click to reveal/hide value" +msgstr "点击显示/隐藏值" + +msgid "Close" +msgstr "关闭" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "以逗号分隔的附加组 ID 列表" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "以逗号分隔的要暴露的端口(例如 6052、8080/udp)" + +msgid "Command" +msgstr "命令" + +msgid "Command to run (space-separated)" +msgstr "运行的命令(以空格分隔)" + +msgid "Configuration" +msgstr "配置" + +msgid "Configure resource limits for this container." +msgstr "为此容器配置资源限制。" + +msgid "Confirm" +msgstr "确认" + +msgid "Connect" +msgstr "连接" + +msgid "Connect to" +msgstr "连接到" + +msgid "Connected successfully to %s" +msgstr "已成功连接到 %s" + +msgid "Connecting container to network" +msgstr "正在将容器连接到网络" + +msgid "Console" +msgstr "控制台" + +msgid "Container" +msgstr "容器" + +msgid "Container Name" +msgstr "容器名称" + +msgid "Container image has no tag - cannot update" +msgstr "容器镜像没有标签 - 无法更新" + +msgid "Container image to use" +msgstr "要使用的容器镜像" + +msgid "Container is not running" +msgstr "容器未在运行" + +msgid "Container updated successfully" +msgstr "容器更新成功" + +msgid "Containers" +msgstr "容器" + +msgid "Containers in pod %s" +msgstr "Pod %s 中的容器" + +msgid "Containers updated successfully" +msgstr "容器更新成功" + +msgid "Containers updated successfully." +msgstr "容器更新成功。" + +msgid "Create" +msgstr "创建" + +msgid "Create %s" +msgstr "创建 %s" + +msgid "Create Command" +msgstr "创建命令" + +msgid "Create Infra Container" +msgstr "创建 Infra 容器" + +msgid "Create new zone (will be named: podman_)" +msgstr "创建新区域(命名为:podman_<网络名称>)" + +msgid "Created" +msgstr "创建时间" + +msgid "Creating %s" +msgstr "正在创建 %s" + +msgid "Creating auto-start configuration for %s" +msgstr "正在为 %s 创建自启动配置" + +msgid "DNS Search Domains" +msgstr "DNS 搜索域" + +msgid "DNS Servers" +msgstr "DNS 服务器" + +msgid "DNS search domains for the infra container" +msgstr "Infra 容器的 DNS 搜索域" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "Infra 容器的 DNS 服务器(与 Pod 中所有容器共享)" + +msgid "Delete" +msgstr "删除" + +msgid "Deleting records: %d/%d" +msgstr "正在删除记录:%d/%d" + +msgid "Deleting records: %s/%s" +msgstr "正在删除记录:%s/%s" + +msgid "Destination" +msgstr "目的地" + +msgid "Device" +msgstr "设备" + +msgid "Disabled" +msgstr "已禁用" + +msgid "Disabling init script" +msgstr "正在禁用初始化脚本" + +msgid "Disconnect container from network" +msgstr "断开容器与网络的连接" + +msgid "Disconnected from %s" +msgstr "已从 %s 断开连接" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "驱动程序" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "特定驱动选项(逗号分隔,例如 type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "启用 IPv6" + +msgid "Enable IPv6 networking" +msgstr "启用 IPv6 网络" + +msgid "Enabling init script" +msgstr "正在启用初始化脚本" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "输入机密数据(密码、令牌、密钥等)" + +msgid "Entrypoint" +msgstr "入口点 (Entrypoint)" + +msgid "Environment Variables" +msgstr "环境变量" + +msgid "Ephemeral (--rm flag)" +msgstr "临时容器(--rm 标志)" + +msgid "Error" +msgstr "错误" + +msgid "Error: %s" +msgstr "错误:%s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "用作父接口的现有物理接口。macvlan/ipvlan 网络必需。" + +msgid "Expecting: %s" +msgstr "期望:%s" + +msgid "Export" +msgstr "导出" + +msgid "Exporting selected volumes: %d/%d" +msgstr "正在导出选定的卷:%d/%d" + +msgid "Expose Ports" +msgstr "暴露端口" + +msgid "Failed to create container" +msgstr "创建容器失败" + +msgid "Fetching information..." +msgstr "正在获取信息..." + +msgid "Find updateable containers" +msgstr "查找可更新的容器" + +msgid "Firewall Zone" +msgstr "防火墙区域" + +msgid "Gateway" +msgstr "网关" + +msgid "Generate" +msgstr "生成" + +msgid "Graph Root" +msgstr "图表根目录 (Graph Root)" + +msgid "Health" +msgstr "健康状态" + +msgid "Hostname" +msgstr "主机名" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Pod 的主机名(默认为 Pod 名称)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "IP 地址" + +msgid "IP Range (CIDR)" +msgstr "IP 范围 (CIDR)" + +msgid "IPv4 Gateway" +msgstr "IPv4 网关" + +msgid "IPv4 Subnet (CIDR)" +msgstr "IPv4 子网 (CIDR)" + +msgid "IPv4 gateway address" +msgstr "IPv4 网关地址" + +msgid "IPv4 subnet in CIDR notation" +msgstr "CIDR 表示法的 IPv4 子网" + +msgid "Image" +msgstr "镜像" + +msgid "Image ID" +msgstr "镜像 ID" + +msgid "Image is already up-to-date" +msgstr "镜像已是最新版本" + +msgid "Image pulled successfully" +msgstr "镜像拉取成功" + +msgid "Images" +msgstr "镜像" + +msgid "Import" +msgstr "导入" + +msgid "Import from archive: %s" +msgstr "从存档导入:%s" + +msgid "Importing volume..." +msgstr "正在导入卷..." + +msgid "Info" +msgstr "信息" + +msgid "Information" +msgstr "信息" + +msgid "Infra Image" +msgstr "Infra 镜像" + +msgid "Init Script" +msgstr "Init 脚本" + +msgid "Inspect" +msgstr "检查 (Inspect)" + +msgid "Int" +msgstr "集成" + +msgid "Interactive" +msgstr "交互式" + +msgid "Interface" +msgstr "接口" + +msgid "Internal Network" +msgstr "内部网络" + +msgid "Labels" +msgstr "标签" + +msgid "Labels in key=value format, one per line" +msgstr "标签格式为 键=值,每行一个" + +msgid "Leave empty to auto-generate" +msgstr "留空以自动生成" + +msgid "Links" +msgstr "链接" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Pod 中容器共享的 Linux 命名空间。默认与 `podman pod create` 一致。" + +msgid "Load" +msgstr "加载" + +msgid "Load System diagnostics" +msgstr "加载系统诊断" + +msgid "Loading init script" +msgstr "正在加载初始化脚本" + +msgid "Loading..." +msgstr "正在加载..." + +msgid "Logs" +msgstr "日志" + +msgid "Manage podman %s" +msgstr "管理 Podman %s" + +msgid "Memory" +msgstr "内存" + +msgid "Memory %" +msgstr "内存 %" + +msgid "Memory + Swap Limit" +msgstr "内存 + 交换限制" + +msgid "Memory Limit" +msgstr "内存限制" + +msgid "Memory Usage" +msgstr "内存使用情况" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "内存限制(例如 512m, 1g)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "内存限制(例如 512m、1g)。-1 = 无限制,0 = 默认。" + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Pod 的内存限制(例如 512m、1g)" + +msgid "Mode" +msgstr "模式" + +msgid "Mount Options" +msgstr "挂载选项" + +msgid "Mountpoint" +msgstr "挂载点" + +msgid "Mounts" +msgstr "挂载" + +msgid "Must be between 0 and 262144" +msgstr "必须介于 0 到 262144 之间" + +msgid "Name" +msgstr "名称" + +msgid "Name for the network" +msgstr "网络名称" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "桥接接口名称(例如 podman0, mynet0)。留空以使用:<网络名称>0。注意:如果生成的名称与现有接口冲突,OpenWrt 将自动递增。" + +msgid "Name updated successfully" +msgstr "名称更新成功" + +msgid "Network" +msgstr "网络" + +msgid "Network I/O" +msgstr "网络 I/O" + +msgid "Network Mode" +msgstr "网络模式" + +msgid "Network Name" +msgstr "网络名称" + +msgid "Network driver" +msgstr "网络驱动" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Pod 的网络命名空间。Pod 中的容器共享此网络。" + +msgid "Networks" +msgstr "网络" + +msgid "Never" +msgstr "从不" + +msgid "No" +msgstr "否" + +msgid "No %s selected" +msgstr "未选择 %s" + +msgid "No containers in this pod" +msgstr "此 Pod 中没有容器" + +msgid "No containers with auto-update label found." +msgstr "沒有找到有自动更新标签的容器。" + +msgid "No new updates available." +msgstr "没有新的更新可用。" + +msgid "No suitable interfaces found" +msgstr "未找到合适的接口" + +msgid "No unused resources found" +msgstr "未找到未使用的资源" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "CPU 数量(例如 0.5, 1.0, 2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "CPU 数量(例如 0.5, 1.0, 2.0)。留空表示无限制。" + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Pod 可用的 CPU 数量(例如 0.5、1.0、2.0)" + +msgid "Number of lines" +msgstr "行数" + +msgid "OK" +msgstr "确定" + +msgid "Old image still used by other containers" +msgstr "旧镜像仍被其他容器使用" + +msgid "On Failure" +msgstr "失败时" + +msgid "One per line, format: host:container[/protocol]" +msgstr "每行一个,格式:主机:容器[/协议]" + +msgid "One per line, format: key=value" +msgstr "每行一个,格式为:键=值" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "每行一个。格式:源:目标[:选项]。选项:ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "OpenWrt 集成已存在\n\n点击移除" + +msgid "OpenWrt integration incomplete" +msgstr "OpenWrt 集成不完整" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "OpenWrt 集成需要指定子网" + +msgid "Override the image used for the infra container" +msgstr "覆盖用于 Infra 容器的镜像" + +msgid "PIDs" +msgstr "PID" + +msgid "Parent Interface" +msgstr "父接口" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "macvlan/ipvlan 网络需要父接口" + +msgid "Part of a pod (\"%s\")" +msgstr "Pod 的一部分(\"%s\")" + +msgid "Pausing pod" +msgstr "正在暂停 Pod" + +msgid "Pod" +msgstr "Pod" + +msgid "Pod Name" +msgstr "Pod 名称" + +msgid "Podman Version" +msgstr "Podman 版本" + +msgid "Pods" +msgstr "Pod" + +msgid "Port Mappings" +msgstr "端口映射" + +msgid "Ports" +msgstr "端口" + +msgid "Privileged" +msgstr "特权" + +msgid "Privileged Mode" +msgstr "特权模式" + +msgid "Processes" +msgstr "进程" + +msgid "Pull Image" +msgstr "拉取镜像" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "拉取未返回镜像 ID - 拉取可能已失败" + +msgid "Pulling %s" +msgstr "正在拉取 %s" + +msgid "Read: %s / Write: %s" +msgstr "读:%s / 写:%s" + +msgid "Refreshing %s..." +msgstr "正在刷新 %s..." + +msgid "Registries" +msgstr "注册表 (Registries)" + +msgid "Registry" +msgstr "注册表" + +msgid "Reload" +msgstr "重新加载" + +msgid "Remove OpenWrt integration" +msgstr "移除 OpenWrt 集成" + +msgid "Remove all unused images (not just dangling)" +msgstr "移除所有未使用的镜像(不只是悬空的)" + +msgid "Remove container" +msgstr "移除容器" + +msgid "Remove unused volumes" +msgstr "移除未使用的卷" + +msgid "Removed: %s" +msgstr "已移除:%s" + +msgid "Removing unused resources, please wait..." +msgstr "正在移除未使用的资源,请稍候..." + +msgid "Repository" +msgstr "仓库" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "共享网络命名空间所必需。仅在确认不需要时禁用。" + +msgid "Resources" +msgstr "资源" + +msgid "Restart Policy" +msgstr "重启策略" + +msgid "Restart container" +msgstr "重启容器" + +msgid "Restart policy updated successfully" +msgstr "重启策略更新成功" + +msgid "Restarting pod" +msgstr "正在重启 Pod" + +msgid "Restrict external access to the network" +msgstr "限制网络的外部访问" + +msgid "Run Root" +msgstr "运行根目录 (Run Root)" + +msgid "Saving resource" +msgstr "正在保存资源" + +msgid "Secret" +msgstr "机密 (Secret)" + +msgid "Secret Data" +msgstr "机密数据" + +msgid "Secret Name" +msgstr "机密名称" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "机密名称只能包含字母、数字、下划线和连字符" + +msgid "Secrets" +msgstr "机密" + +msgid "Security Notice:" +msgstr "安全声明:" + +msgid "Select containers to update" +msgstr "选择要更新的容器" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "为新容器选择网络。用户创建的网络可提供更好的隔离性和容器间 DNS 解析。" + +msgid "Select what to clean up:" +msgstr "选择要清理的内容:" + +msgid "Sensitive data is hidden for security reasons." +msgstr "出于安全原因,敏感数据已隐藏。" + +msgid "Setup OpenWrt Integration" +msgstr "设置 OpenWrt 集成" + +msgid "Shared Namespaces" +msgstr "共享命名空间" + +msgid "Shares network with another container" +msgstr "与其他容器共享网络" + +msgid "Show" +msgstr "显示" + +msgid "Since" +msgstr "自" + +msgid "Size" +msgstr "大小" + +msgid "Socket Path" +msgstr "Socket 路径" + +msgid "Source" +msgstr "源" + +msgid "Space freed: %s" +msgstr "已释放空间:%s" + +msgid "Start after creation" +msgstr "创建后启动" + +msgid "Start container" +msgstr "启动容器" + +msgid "Start on boot" +msgstr "开机启动" + +msgid "Start script disabled" +msgstr "启动脚本已禁用" + +msgid "Start script missing\n\nClick to generate" +msgstr "启动脚本缺失\n\n点击生成" + +msgid "Start stream" +msgstr "开始流" + +msgid "Started" +msgstr "启动时间" + +msgid "Started At" +msgstr "启动时间" + +msgid "Starting pod" +msgstr "正在启动 Pod" + +msgid "Statistics" +msgstr "统计" + +msgid "Stats" +msgstr "统计" + +msgid "Status" +msgstr "状态" + +msgid "Stop container" +msgstr "停止容器" + +msgid "Stop stream" +msgstr "停止流" + +msgid "Stopping pod" +msgstr "正在停止 Pod" + +msgid "Subnet" +msgstr "子网" + +msgid "Supplementary Groups" +msgstr "附加组" + +msgid "System Diagnostics" +msgstr "系统诊断" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "标签" + +msgid "The sensitive data to store securely" +msgstr "要安全存储的敏感数据" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "要为容器启用自动更新,添加标签:io.containers.autoupdate=registry" + +msgid "Total" +msgstr "总计" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "总内存限制(内存 + swap)。-1 = 无限制,0 = 默认。" + +msgid "Type" +msgstr "类型" + +msgid "Unknown" +msgstr "未知" + +msgid "Unless Stopped" +msgstr "除非已停止 (Unless Stopped)" + +msgid "Until" +msgstr "直到" + +msgid "Update" +msgstr "更新" + +msgid "Update Resources" +msgstr "更新资源" + +msgid "Update name" +msgstr "更新名称" + +msgid "Update restart policy" +msgstr "更新重启策略" + +msgid "Updating Images" +msgstr "正在更新镜像" + +msgid "Updating container" +msgstr "正在更新容器" + +msgid "Upload failed: %s" +msgstr "上传失败:%s" + +msgid "Uploading file..." +msgstr "正在上传文件..." + +msgid "User" +msgstr "用户" + +msgid "User and group to run as (UID:GID)" +msgstr "运行用户和组 (UID:GID)" + +msgid "Value" +msgstr "值" + +msgid "Variable" +msgstr "变量" + +msgid "Volume" +msgstr "卷" + +msgid "Volume Name" +msgstr "卷名称" + +msgid "Volume driver to use" +msgstr "要使用的卷驱动" + +msgid "Volume imported successfully" +msgstr "卷导入成功" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "卷名称。留空以自动生成。" + +msgid "Volumes" +msgstr "卷" + +msgid "Working Directory" +msgstr "工作目录" + +msgid "Yes" +msgstr "是" + +msgid "default" +msgstr "默认" + +msgid "non-empty value" +msgstr "非空值" + +msgid "running" +msgstr "正在运行" + +msgid "total" +msgstr "总计" + +msgid "Are you sure to remove pod?" +msgstr "确定要删除 Pod 吗?" + +msgid "Cgroup parent" +msgstr "Cgroup 父级" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "CPU %" + +msgid "CPU period" +msgstr "CPU 周期" + +msgid "CPU quota" +msgstr "CPU 配额" + +msgid "CPU set" +msgstr "CPU 集合" + +msgid "CPU shares" +msgstr "CPU 份额" + +msgid "DNS options" +msgstr "DNS 选项" + +msgid "DNS search" +msgstr "DNS 搜索" + +msgid "DNS servers" +msgstr "DNS 服务器" + +msgid "Exit policy" +msgstr "退出策略" + +msgid "Extra hosts" +msgstr "额外主机" + +msgid "Host network" +msgstr "主机网络" + +msgid "Infra container" +msgstr "Infra 容器" + +msgid "Memory limit" +msgstr "内存限制" + +msgid "Net I/O" +msgstr "网络 I/O" + +msgid "Pause pod" +msgstr "暂停 Pod" + +msgid "Pod is not running" +msgstr "Pod 未运行" + +msgid "Remove pod" +msgstr "删除 Pod" + +msgid "Restart policy" +msgstr "重启策略" + +msgid "Restart pod" +msgstr "重启 Pod" + +msgid "Shared namespaces" +msgstr "共享命名空间" + +msgid "Start pod" +msgstr "启动 Pod" + +msgid "Static IPv4" +msgstr "静态 IPv4" + +msgid "Static MAC" +msgstr "静态 MAC" + +msgid "Stop pod" +msgstr "停止 Pod" + +msgid "Unlimited" +msgstr "无限制" + +msgid "Unpause pod" +msgstr "恢复 Pod" + +msgid "Stopping container" +msgstr "正在停止容器" + +msgid "Stop failed: %s" +msgstr "停止失败:%s" + +msgid "Removing container" +msgstr "正在删除容器" + +msgid "Remove failed: %s" +msgstr "删除失败:%s" + +msgid "Creating container" +msgstr "正在创建容器" + +msgid "Create failed: %s" +msgstr "创建失败:%s" + +msgid "Starting container" +msgstr "正在启动容器" + +msgid "Start failed: %s" +msgstr "启动失败:%s" + +msgid "Reinstating init script" +msgstr "正在恢复初始化脚本" + +msgid "Init script reinstate failed: %s" +msgstr "恢复初始化脚本失败:%s" + +msgid "Updated to %s" +msgstr "已更新到 %s" + +msgid "Update image: %d/%d - %s" +msgstr "更新镜像:%d/%d - %s" + +msgid "Removed old image %s" +msgstr "已删除旧镜像 %s" + +msgid "Done." +msgstr "完成。" + +msgid "Pause container" +msgstr "暂停容器" + +msgid "Unpause container" +msgstr "恢复容器" diff --git a/luci-app-podman/po/zh_Hant/podman.po b/luci-app-podman/po/zh_Hant/podman.po new file mode 100644 index 000000000..fa7eca0d3 --- /dev/null +++ b/luci-app-podman/po/zh_Hant/podman.po @@ -0,0 +1,1090 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-app-podman\n" +"PO-Revision-Date: 2026-05-03 11:05+0000\n" +"Last-Translator:\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" + +msgid "%d check(s) failed:" +msgstr "%d 次檢查失敗:" + +msgid "%d updated, %d failed:" +msgstr "%d 已更新,%d 失敗:" + +msgid "%s created successfully" +msgstr "%s 成功建立" + +msgid "%s: %d/%d" +msgstr "%s:%d/%d" + +msgid "(none)" +msgstr "(無)" + +msgid "-- Select %s --" +msgstr "-- 選擇 %s --" + +msgid "1-253 characters: letters, numbers, underscore (_), hyphen (-) only" +msgstr "1-253 字元:僅限英文、數字、底線(_)、破折號(-)" + +msgid "512m, 1g, or -1 for unlimited" +msgstr "512m、1g 或 -1(無限制)" + +msgid "API Version" +msgstr "API 版本" + +msgid "Add the container to an existing pod. Network, hostname and port mappings come from the pod's infra container." +msgstr "將容器加入現有 Pod。網路、主機名稱和連接埠對應來自 Pod 的 infra 容器。" + +msgid "All images updated successfully" +msgstr "所有映像更新成功" + +msgid "All volumes exported successfully" +msgstr "所有儲存區都成功匯出" + +msgid "Allocate TTY (-t)" +msgstr "配發 TTY(-t)" + +msgid "Allocate container IP from this range" +msgstr "從這個範圍配發容器 IP 地址" + +msgid "Always" +msgstr "總是" + +msgid "Apply Updates" +msgstr "套用更新" + +msgid "Are you sure to delete the records?" +msgstr "確定刪除這些記錄嗎?" + +msgid "Are you sure to remove container?" +msgstr "確定移除此容器嗎?" + +msgid "Are you sure to remove integration?" +msgstr "確定移除此整合嗎?" + +msgid "Auto Remove (--rm)" +msgstr "自動移除(--rm)" + +msgid "Auto Update" +msgstr "自動更新" + +msgid "Auto-Update" +msgstr "自動更新" + +msgid "Automatically configure OpenWrt network interface, bridge, and firewall zone. Highly recommended for proper container networking on OpenWrt." +msgstr "自動設定 OpenWrt 網路界面、橋接器、以及防火牆區域。強烈建議啟用,在 OpenWrt 上建立合適的容器網路。" + +msgid "Automatically start the container after it is created" +msgstr "建立容器後自動啟動" + +msgid "Automatically update container when newer image is available. Adds label: io.containers.autoupdate=registry" +msgstr "新的映像檔可用時自動更新容器。增加標籤:io.containers.autoupdate=registry" + +msgid "Basic Information" +msgstr "基本資訊" + +msgid "Block I/O" +msgstr "區塊 I/O" + +msgid "Boot" +msgstr "開機" + +msgid "Bridge Interface Name" +msgstr "橋接界面名稱" + +msgid "CPU" +msgstr "中央處理器" + +msgid "CPU Limit" +msgstr "CPU 上限" + +msgid "CPU Set" +msgstr "CPU 集合" + +msgid "CPU Shares Weight" +msgstr "CPU 分享權重" + +msgid "CPU Usage" +msgstr "CPU 用量" + +msgid "CPU shares (relative weight). 0 = use default." +msgstr "CPU 份額(相對權重)。0 = 使用預設值。" + +msgid "CPUs the pod is allowed to run on (e.g., \"0-3\" or \"0,2\")" +msgstr "允許 Pod 執行的 CPU(例如 \"0-3\" 或 \"0,2\")" + +msgid "Cancel" +msgstr "取消" + +msgid "Check" +msgstr "檢查" + +msgid "Check %d containers for updates?" +msgstr "檢查 %d 個容器的更新?" + +msgid "Check Complete" +msgstr "檢查完成" + +msgid "Check for Updates" +msgstr "檢查更新" + +msgid "Check for updates: %s/%s" +msgstr "檢查更新:%s/%s" + +msgid "Choose firewall zone for this network." +msgstr "為此網路選擇防火牆區域。" + +msgid "Clean Up Now" +msgstr "現在清理" + +msgid "Cleaning up old images..." +msgstr "正在清理舊映像..." + +msgid "Cleanup / Prune" +msgstr "清理 / 裁剪" + +msgid "Cleanup Complete" +msgstr "清理完成" + +msgid "Cleanup Unused Resources" +msgstr "清理非使用中的資源" + +msgid "Cleanup successful!" +msgstr "清理成功!" + +msgid "Clear logs" +msgstr "清除日誌" + +msgid "Click icon to repair" +msgstr "點擊圖示進行修復" + +msgid "Click to disconnect" +msgstr "點擊斷開連接" + +msgid "Click to enable/disable" +msgstr "點擊啟用/禁用" + +msgid "Click to reveal/hide value" +msgstr "點擊以顯示/隱藏數值" + +msgid "Close" +msgstr "關閉" + +msgid "Comma-separated list of supplementary group IDs" +msgstr "以逗號分隔的附加組 ID 列表" + +msgid "Comma-separated ports to expose (e.g., 6052, 8080/udp)" +msgstr "以逗號分隔的要暴露的端口(例如 6052、8080/udp)" + +msgid "Command" +msgstr "指令" + +msgid "Command to run (space-separated)" +msgstr "要執行的指令(使用空格字元分隔)" + +msgid "Configuration" +msgstr "設定" + +msgid "Configure resource limits for this container." +msgstr "為此容器配置資源限制。" + +msgid "Confirm" +msgstr "確認" + +msgid "Connect" +msgstr "連線" + +msgid "Connect to" +msgstr "連線至" + +msgid "Connected successfully to %s" +msgstr "已成功連接到 %s" + +msgid "Connecting container to network" +msgstr "正在將容器連接到網路" + +msgid "Console" +msgstr "終端機" + +msgid "Container" +msgstr "容器" + +msgid "Container Name" +msgstr "容器名稱" + +msgid "Container image has no tag - cannot update" +msgstr "容器映像沒有標籤 - 無法更新" + +msgid "Container image to use" +msgstr "要使用的容器映像檔" + +msgid "Container is not running" +msgstr "容器未在運行" + +msgid "Container updated successfully" +msgstr "容器更新成功" + +msgid "Containers" +msgstr "容器" + +msgid "Containers in pod %s" +msgstr "Pod %s 中的容器" + +msgid "Containers updated successfully" +msgstr "容器更新成功" + +msgid "Containers updated successfully." +msgstr "容器更新成功。" + +msgid "Create" +msgstr "建立" + +msgid "Create %s" +msgstr "建立 %s" + +msgid "Create Command" +msgstr "建立命令" + +msgid "Create Infra Container" +msgstr "建立 Infra 容器" + +msgid "Create new zone (will be named: podman_)" +msgstr "建立新的區域(會命名為:podman_)" + +msgid "Created" +msgstr "已建立" + +msgid "Creating %s" +msgstr "正在建立 %s" + +msgid "Creating auto-start configuration for %s" +msgstr "正在為 %s 建立自動啟動設定" + +msgid "DNS Search Domains" +msgstr "DNS 搜尋網域" + +msgid "DNS Servers" +msgstr "DNS 伺服器" + +msgid "DNS search domains for the infra container" +msgstr "Infra 容器的 DNS 搜尋網域" + +msgid "DNS servers for the infra container (shared with all pod containers)" +msgstr "Infra 容器的 DNS 伺服器(與 Pod 中所有容器共用)" + +msgid "Delete" +msgstr "刪除" + +msgid "Deleting records: %d/%d" +msgstr "正在刪除記錄:%d/%d" + +msgid "Deleting records: %s/%s" +msgstr "正在刪除記錄:%s/%s" + +msgid "Destination" +msgstr "目標" + +msgid "Device" +msgstr "裝置" + +msgid "Disabled" +msgstr "已停用" + +msgid "Disabling init script" +msgstr "正在禁用初始化腳本" + +msgid "Disconnect container from network" +msgstr "斷開容器與網路的連接" + +msgid "Disconnected from %s" +msgstr "已從 %s 斷開連接" + +msgid "Dnsmasq" +msgstr "Dnsmasq" + +msgid "Driver" +msgstr "驅動程式" + +msgid "Driver-specific options (comma-separated, e.g., type=tmpfs,o=size=100m)" +msgstr "驅動程式相關選項(逗號分隔,例如 type=tmpfs,o=size=100m)" + +msgid "Enable IPv6" +msgstr "啟用 IPv6" + +msgid "Enable IPv6 networking" +msgstr "啟用 IPv6 網路" + +msgid "Enabling init script" +msgstr "正在啟用初始化腳本" + +msgid "Enter secret data (password, token, key, etc.)" +msgstr "輸入私密資料(密碼、令牌、金鑰… 等等)" + +msgid "Entrypoint" +msgstr "進入點" + +msgid "Environment Variables" +msgstr "環境變數" + +msgid "Ephemeral (--rm flag)" +msgstr "臨時容器(--rm 標誌)" + +msgid "Error" +msgstr "錯誤" + +msgid "Error: %s" +msgstr "錯誤:%s" + +msgid "Existing physical interface to use as parent. Required for macvlan/ipvlan networks." +msgstr "用來當作母設備的實體界面,macvlan/ipvlan 需要它。" + +msgid "Expecting: %s" +msgstr "期望:%s" + +msgid "Export" +msgstr "匯出" + +msgid "Exporting selected volumes: %d/%d" +msgstr "正在匯出選定的儲存區:%d/%d" + +msgid "Expose Ports" +msgstr "暴露端口" + +msgid "Failed to create container" +msgstr "建立容器失敗" + +msgid "Fetching information..." +msgstr "獲取資訊…" + +msgid "Find updateable containers" +msgstr "尋找可更新的容器" + +msgid "Firewall Zone" +msgstr "防火牆區域" + +msgid "Gateway" +msgstr "閘道器" + +msgid "Generate" +msgstr "產生" + +msgid "Graph Root" +msgstr "儲存圖根目錄" + +msgid "Health" +msgstr "健康度" + +msgid "Hostname" +msgstr "主機名稱" + +msgid "Hostname for the pod (defaults to pod name)" +msgstr "Pod 的主機名稱(預設為 Pod 名稱)" + +msgid "ID" +msgstr "ID" + +msgid "IP Address" +msgstr "IP 地址" + +msgid "IP Range (CIDR)" +msgstr "IP 範圍(CIDR)" + +msgid "IPv4 Gateway" +msgstr "IPv4 閘道器" + +msgid "IPv4 Subnet (CIDR)" +msgstr "IPv4 子網域(CIDR)" + +msgid "IPv4 gateway address" +msgstr "IPv4 閘道器地址" + +msgid "IPv4 subnet in CIDR notation" +msgstr "CIDR 註記的 IPv4 子網域" + +msgid "Image" +msgstr "映像檔" + +msgid "Image ID" +msgstr "映像檔 ID" + +msgid "Image is already up-to-date" +msgstr "映像已是最新版本" + +msgid "Image pulled successfully" +msgstr "映像檔成功拉回" + +msgid "Images" +msgstr "映像檔" + +msgid "Import" +msgstr "匯入" + +msgid "Import from archive: %s" +msgstr "從存檔導入:%s" + +msgid "Importing volume..." +msgstr "正在匯入儲存區…" + +msgid "Info" +msgstr "資訊" + +msgid "Information" +msgstr "資訊" + +msgid "Infra Image" +msgstr "Infra 映像檔" + +msgid "Init Script" +msgstr "初始化指令稿" + +msgid "Inspect" +msgstr "檢視" + +msgid "Int" +msgstr "整數" + +msgid "Interactive" +msgstr "互動式" + +msgid "Interface" +msgstr "介面" + +msgid "Internal Network" +msgstr "內部網路" + +msgid "Labels" +msgstr "標籤" + +msgid "Labels in key=value format, one per line" +msgstr "使用 key=value 格式的標籤,一行一個" + +msgid "Leave empty to auto-generate" +msgstr "留空會自動產生" + +msgid "Links" +msgstr "連結" + +msgid "Linux namespaces shared between containers in the pod. Default matches `podman pod create`." +msgstr "Pod 中容器共用的 Linux 命名空間。預設與 `podman pod create` 一致。" + +msgid "Load" +msgstr "載入" + +msgid "Load System diagnostics" +msgstr "載入系統診斷" + +msgid "Loading init script" +msgstr "正在載入初始化腳本" + +msgid "Loading..." +msgstr "正在載入…" + +msgid "Logs" +msgstr "日誌" + +msgid "Manage podman %s" +msgstr "管理 Podman %s" + +msgid "Memory" +msgstr "記憶體" + +msgid "Memory %" +msgstr "記憶體 %" + +msgid "Memory + Swap Limit" +msgstr "記憶體 + 置換區限制" + +msgid "Memory Limit" +msgstr "記憶體限制" + +msgid "Memory Usage" +msgstr "記憶體用量" + +msgid "Memory limit (e.g., 512m, 1g)" +msgstr "記憶體限制(例如 512m、1g)" + +msgid "Memory limit (e.g., 512m, 1g). -1 = unlimited, 0 = use default." +msgstr "記憶體限制(例如 512m、1g)。-1 = 無限制,0 = 預設。" + +msgid "Memory limit for the pod (e.g., 512m, 1g)" +msgstr "Pod 的記憶體限制(例如 512m、1g)" + +msgid "Mode" +msgstr "模式" + +msgid "Mount Options" +msgstr "掛載選項" + +msgid "Mountpoint" +msgstr "掛載點" + +msgid "Mounts" +msgstr "掛載" + +msgid "Must be between 0 and 262144" +msgstr "必須介於 0 與 262144 之間" + +msgid "Name" +msgstr "名稱" + +msgid "Name for the network" +msgstr "網路的名稱" + +msgid "Name of the bridge interface (e.g., podman0, mynet0). Leave empty to use: <network-name>0. Note: If the generated name conflicts with an existing interface, OpenWrt will auto-increment it." +msgstr "橋接器界面的名稱(例如 podman0、mynet0)。留空使用 <network-name>0。注意:如果自動產生的名稱與現有界面名稱衝突,OpenWrt 會自動增加編號。" + +msgid "Name updated successfully" +msgstr "名稱更新成功" + +msgid "Network" +msgstr "網路" + +msgid "Network I/O" +msgstr "網路 I/O" + +msgid "Network Mode" +msgstr "網路模式" + +msgid "Network Name" +msgstr "網路名稱" + +msgid "Network driver" +msgstr "網路驅動程式" + +msgid "Network namespace for the pod. Containers in the pod share this network." +msgstr "Pod 的網路命名空間。Pod 中的容器共用此網路。" + +msgid "Networks" +msgstr "網路" + +msgid "Never" +msgstr "永不" + +msgid "No" +msgstr "否" + +msgid "No %s selected" +msgstr "沒有 %s 被選擇" + +msgid "No containers in this pod" +msgstr "此 Pod 中沒有容器" + +msgid "No containers with auto-update label found." +msgstr "沒有找到有自動更新標籤的容器。" + +msgid "No new updates available." +msgstr "沒有新的更新可用。" + +msgid "No suitable interfaces found" +msgstr "沒有適合的界面" + +msgid "No unused resources found" +msgstr "沒有非使用中的資源" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0)" +msgstr "CPU 數量(例如 0.5、1.0、2.0)" + +msgid "Number of CPUs (e.g., 0.5, 1.0, 2.0). Leave empty for unlimited." +msgstr "CPU 數量(例如 0.5、1.0、2.0)。留空表示無上限。" + +msgid "Number of CPUs available to the pod (e.g., 0.5, 1.0, 2.0)" +msgstr "Pod 可用的 CPU 數量(例如 0.5、1.0、2.0)" + +msgid "Number of lines" +msgstr "行數" + +msgid "OK" +msgstr "確定" + +msgid "Old image still used by other containers" +msgstr "舊映像檔仍被其他容器使用" + +msgid "On Failure" +msgstr "於錯誤發生時" + +msgid "One per line, format: host:container[/protocol]" +msgstr "每行一個,格式:主機:容器[/協議]" + +msgid "One per line, format: key=value" +msgstr "一行一個,格式 key=value" + +msgid "One per line. Format: source:destination[:options]. Options: ro, rw, Z, z" +msgstr "每行一個。格式:源:目標[:選項]。選項:ro, rw, Z, z" + +msgid "OpenWrt integration exists\n\nClick to remove" +msgstr "OpenWrt 整合已存在\n\n點擊移除" + +msgid "OpenWrt integration incomplete" +msgstr "OpenWrt 整合不完整" + +msgid "OpenWrt integration requires subnet to be specified" +msgstr "OpenWrt 整合需要指定子網路" + +msgid "Override the image used for the infra container" +msgstr "覆寫用於 Infra 容器的映像檔" + +msgid "PIDs" +msgstr "PIDs" + +msgid "Parent Interface" +msgstr "母界面" + +msgid "Parent interface is required for macvlan/ipvlan networks" +msgstr "macvlan/ipvlan 網路需要母界面" + +msgid "Part of a pod (\"%s\")" +msgstr "Pod 的一部分(\"%s\")" + +msgid "Pausing pod" +msgstr "正在暫停 Pod" + +msgid "Pod" +msgstr "容器群組" + +msgid "Pod Name" +msgstr "Pod 名稱" + +msgid "Podman Version" +msgstr "Podman 版本" + +msgid "Pods" +msgstr "容器群組" + +msgid "Port Mappings" +msgstr "連接埠對應" + +msgid "Ports" +msgstr "端口" + +msgid "Privileged" +msgstr "有特權的" + +msgid "Privileged Mode" +msgstr "特權模式" + +msgid "Processes" +msgstr "進程" + +msgid "Pull Image" +msgstr "獲取映像檔" + +msgid "Pull did not return an image ID - pull may have failed" +msgstr "拉取未返回映像 ID - 拉取可能已失敗" + +msgid "Pulling %s" +msgstr "正在拉取 %s" + +msgid "Read: %s / Write: %s" +msgstr "讀取:%s / 寫入:%s" + +msgid "Refreshing %s..." +msgstr "正在更新 %s…" + +msgid "Registries" +msgstr "註冊表" + +msgid "Registry" +msgstr "註冊表" + +msgid "Reload" +msgstr "重新載入" + +msgid "Remove OpenWrt integration" +msgstr "移除 OpenWrt 整合" + +msgid "Remove all unused images (not just dangling)" +msgstr "移除所有未使用的映像檔(而非只是空懸的)" + +msgid "Remove container" +msgstr "移除容器" + +msgid "Remove unused volumes" +msgstr "移除未使用的儲存區" + +msgid "Removed: %s" +msgstr "移除了:%s" + +msgid "Removing unused resources, please wait..." +msgstr "正在移除未使用的資源,請稍候…" + +msgid "Repository" +msgstr "儲存庫" + +msgid "Required for shared network namespace. Disable only if you know you do not need it." +msgstr "共用網路命名空間所必需。僅在確認不需要時停用。" + +msgid "Resources" +msgstr "資源" + +msgid "Restart Policy" +msgstr "重新啟動原則" + +msgid "Restart container" +msgstr "重啟容器" + +msgid "Restart policy updated successfully" +msgstr "重新啟動原則成功更新" + +msgid "Restarting pod" +msgstr "正在重新啟動 Pod" + +msgid "Restrict external access to the network" +msgstr "限制對外部網路的操作" + +msgid "Run Root" +msgstr "執行時根目錄" + +msgid "Saving resource" +msgstr "正在儲存資源" + +msgid "Secret" +msgstr "私密資料" + +msgid "Secret Data" +msgstr "私密資料" + +msgid "Secret Name" +msgstr "私密資料名稱" + +msgid "Secret name can only contain letters, numbers, underscores, and hyphens" +msgstr "私密資料名稱只能包含字母、數字、底線、及破折號" + +msgid "Secrets" +msgstr "私密資料" + +msgid "Security Notice:" +msgstr "安全性警告:" + +msgid "Select containers to update" +msgstr "選擇要更新的容器" + +msgid "Select network for the new container. User-created networks provide better isolation and DNS resolution between containers." +msgstr "為新容器選擇網路。用戶建立的網路可提供更好的隔離性和容器間 DNS 解析。" + +msgid "Select what to clean up:" +msgstr "選擇要清理的項目:" + +msgid "Sensitive data is hidden for security reasons." +msgstr "敏感資料因安全緣故隱藏。" + +msgid "Setup OpenWrt Integration" +msgstr "設定 OpenWrt 整合" + +msgid "Shared Namespaces" +msgstr "共用命名空間" + +msgid "Shares network with another container" +msgstr "與其他容器共享網路" + +msgid "Show" +msgstr "顯示" + +msgid "Since" +msgstr "自" + +msgid "Size" +msgstr "大小" + +msgid "Socket Path" +msgstr "通訊端路徑" + +msgid "Source" +msgstr "來源" + +msgid "Space freed: %s" +msgstr "釋放的空間:%s" + +msgid "Start after creation" +msgstr "建立後啟動" + +msgid "Start container" +msgstr "啟動容器" + +msgid "Start on boot" +msgstr "開機啟動" + +msgid "Start script disabled" +msgstr "啟動腳本已禁用" + +msgid "Start script missing\n\nClick to generate" +msgstr "啟動腳本缺失\n\n點擊生成" + +msgid "Start stream" +msgstr "開始串流" + +msgid "Started" +msgstr "已啟動" + +msgid "Started At" +msgstr "啟動時間" + +msgid "Starting pod" +msgstr "正在啟動 Pod" + +msgid "Statistics" +msgstr "統計" + +msgid "Stats" +msgstr "狀態" + +msgid "Status" +msgstr "狀態" + +msgid "Stop container" +msgstr "停止容器" + +msgid "Stop stream" +msgstr "停止串流" + +msgid "Stopping pod" +msgstr "正在停止 Pod" + +msgid "Subnet" +msgstr "子網路" + +msgid "Supplementary Groups" +msgstr "附加組" + +msgid "System Diagnostics" +msgstr "系統診斷" + +msgid "TTY" +msgstr "TTY" + +msgid "Tag" +msgstr "標籤" + +msgid "The sensitive data to store securely" +msgstr "要被安全保存的敏感資料" + +msgid "To enable auto-update for a container, add the label: io.containers.autoupdate=registry" +msgstr "要啟用容器的自動更新,增加這個標籤:io.containers.autoupdate=registry" + +msgid "Total" +msgstr "總計" + +msgid "Total memory limit (memory + swap). -1 = unlimited, 0 = use default." +msgstr "總記憶體限制(記憶體 + swap)。-1 = 無限制,0 = 預設。" + +msgid "Type" +msgstr "類型" + +msgid "Unknown" +msgstr "未知" + +msgid "Unless Stopped" +msgstr "除非停止" + +msgid "Until" +msgstr "直到" + +msgid "Update" +msgstr "更新" + +msgid "Update Resources" +msgstr "更新資源" + +msgid "Update name" +msgstr "更新名稱" + +msgid "Update restart policy" +msgstr "更新重啟策略" + +msgid "Updating Images" +msgstr "正在更新映像" + +msgid "Updating container" +msgstr "正在更新容器" + +msgid "Upload failed: %s" +msgstr "上傳失敗:%s" + +msgid "Uploading file..." +msgstr "正在上傳文件..." + +msgid "User" +msgstr "使用者" + +msgid "User and group to run as (UID:GID)" +msgstr "運行用戶和組 (UID:GID)" + +msgid "Value" +msgstr "值" + +msgid "Variable" +msgstr "變數" + +msgid "Volume" +msgstr "儲存區" + +msgid "Volume Name" +msgstr "儲存區名稱" + +msgid "Volume driver to use" +msgstr "要使用的儲存區驅動器" + +msgid "Volume imported successfully" +msgstr "儲存區成功匯入" + +msgid "Volume name. Leave empty to auto-generate." +msgstr "儲存區名稱。留空會自動產生。" + +msgid "Volumes" +msgstr "儲存區" + +msgid "Working Directory" +msgstr "工作目錄" + +msgid "Yes" +msgstr "是" + +msgid "default" +msgstr "預設" + +msgid "non-empty value" +msgstr "非空值" + +msgid "running" +msgstr "執行中" + +msgid "total" +msgstr "總共" + +msgid "Are you sure to remove pod?" +msgstr "確定要移除 Pod 嗎?" + +msgid "Cgroup parent" +msgstr "Cgroup 父層" + +msgid "CID" +msgstr "CID" + +msgid "CPU %" +msgstr "CPU %" + +msgid "CPU period" +msgstr "CPU 週期" + +msgid "CPU quota" +msgstr "CPU 配額" + +msgid "CPU set" +msgstr "CPU 集合" + +msgid "CPU shares" +msgstr "CPU 份額" + +msgid "DNS options" +msgstr "DNS 選項" + +msgid "DNS search" +msgstr "DNS 搜尋" + +msgid "DNS servers" +msgstr "DNS 伺服器" + +msgid "Exit policy" +msgstr "結束原則" + +msgid "Extra hosts" +msgstr "額外主機" + +msgid "Host network" +msgstr "主機網路" + +msgid "Infra container" +msgstr "Infra 容器" + +msgid "Memory limit" +msgstr "記憶體限制" + +msgid "Net I/O" +msgstr "網路 I/O" + +msgid "Pause pod" +msgstr "暫停 Pod" + +msgid "Pod is not running" +msgstr "Pod 未在運行" + +msgid "Remove pod" +msgstr "移除 Pod" + +msgid "Restart policy" +msgstr "重新啟動原則" + +msgid "Restart pod" +msgstr "重新啟動 Pod" + +msgid "Shared namespaces" +msgstr "共用命名空間" + +msgid "Start pod" +msgstr "啟動 Pod" + +msgid "Static IPv4" +msgstr "靜態 IPv4" + +msgid "Static MAC" +msgstr "靜態 MAC" + +msgid "Stop pod" +msgstr "停止 Pod" + +msgid "Unlimited" +msgstr "無限制" + +msgid "Unpause pod" +msgstr "恢復 Pod" + +msgid "Stopping container" +msgstr "正在停止容器" + +msgid "Stop failed: %s" +msgstr "停止失敗:%s" + +msgid "Removing container" +msgstr "正在移除容器" + +msgid "Remove failed: %s" +msgstr "移除失敗:%s" + +msgid "Creating container" +msgstr "正在建立容器" + +msgid "Create failed: %s" +msgstr "建立失敗:%s" + +msgid "Starting container" +msgstr "正在啟動容器" + +msgid "Start failed: %s" +msgstr "啟動失敗:%s" + +msgid "Reinstating init script" +msgstr "正在還原初始化指令稿" + +msgid "Init script reinstate failed: %s" +msgstr "還原初始化指令稿失敗:%s" + +msgid "Updated to %s" +msgstr "已更新至 %s" + +msgid "Update image: %d/%d - %s" +msgstr "更新映像檔:%d/%d - %s" + +msgid "Removed old image %s" +msgstr "已移除舊映像檔 %s" + +msgid "Done." +msgstr "完成。" + +msgid "Pause container" +msgstr "暫停容器" + +msgid "Unpause container" +msgstr "恢復容器" diff --git a/luci-app-podman/root/etc/config/luci-podman-opkg b/luci-app-podman/root/etc/config/luci-podman-opkg new file mode 100644 index 000000000..52ae83bfc --- /dev/null +++ b/luci-app-podman/root/etc/config/luci-podman-opkg @@ -0,0 +1,3 @@ +config luci_podman 'globals' + option init_start_priority '100' + option socket_path 'unix:///run/podman/podman.sock' diff --git a/luci-app-podman/root/etc/uci-defaults/80_luci-app-podman b/luci-app-podman/root/etc/uci-defaults/80_luci-app-podman new file mode 100755 index 000000000..a5d245bd8 --- /dev/null +++ b/luci-app-podman/root/etc/uci-defaults/80_luci-app-podman @@ -0,0 +1,10 @@ +#!/bin/sh + +# Handle config: podman-opkg is the shipped default, podman is the live config +if [ ! -f /etc/config/luci-podman ]; then + mv /etc/config/luci-podman-opkg /etc/config/luci-podman +else + echo "Existing /etc/config/luci-podman preserved. New defaults at /etc/config/luci-podman-opkg" +fi + +exit 0 diff --git a/luci-app-podman/root/usr/libexec/podman-api b/luci-app-podman/root/usr/libexec/podman-api new file mode 100755 index 000000000..18284eda3 --- /dev/null +++ b/luci-app-podman/root/usr/libexec/podman-api @@ -0,0 +1,103 @@ +#!/usr/bin/env ucode + +'use strict'; + +import { popen, writefile, unlink } from 'fs'; +import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable +import { validate_int, validate_name, require_param } from 'luci.podman_validate'; // ucode-lsp disable + +function die(msg) { + warn(`${msg}\n`); + exit(1); +} + +// Volume export/import here shells out to the local `podman` CLI, which can +// only reach a Podman daemon running on the same host. When the user has +// configured a remote TCP Podman, refuse upfront rather than failing weirdly. +if (podman_socket.is_remote()) + die(`Volume operations are not supported with a remote Podman (${podman_socket.get_dest()}); the CLI helper requires a local daemon.`); + +// --- Main --- + +let action = ARGV[0]; + +if (action === 'volume_export') { + let volumeName = ARGV[1]; + let err = require_param('volumeName', volumeName) || validate_name(volumeName); + if (err) die(err); + + // volume export outputs a tar stream - use podman CLI directly + let escaped = replace(volumeName, /'/g, "'\\''"); + let p = popen(`/usr/bin/podman volume export '${escaped}'`, 'r'); + if (!p) + die('Failed to run podman volume export'); + + let chunk; + while ((chunk = p.read(65536)) != null && length(chunk) > 0) + print(chunk); + p.close(); +} +else if (action === 'volume_import') { + let volumeName = ARGV[1]; + let compressed = ARGV[2]; + let volumeData = ARGV[3]; + + let err = require_param('volumeName', volumeName) || validate_name(volumeName) + || require_param('volumeData', volumeData) + || require_param('compressed', compressed) || validate_int(compressed); + if (err) die(err); + + // Validate base64 content + if (!match(volumeData, /^[A-Za-z0-9+\/]*={0,2}$/) || (length(volumeData) % 4) !== 0) + die('Invalid Base64 content'); + + let decoded = b64dec(volumeData); + if (decoded == null) + die('Failed to decode import data'); + + // Decompress if needed + if (compressed === '1') { + let tp = popen('mktemp /tmp/podman_import_XXXXXX', 'r'); + let tmppath = tp ? trim(tp.read('all') || '') : ''; + if (tp) tp.close(); + if (!tmppath) + die('Failed to create temp file'); + + writefile(tmppath, decoded); + + let p = popen(`gunzip -c '${tmppath}'`, 'r'); + decoded = p ? p.read('all') : null; + if (p) p.close(); + unlink(tmppath); + + if (!decoded) + die('Failed to decompress import data'); + } + + // Ensure volume exists + let escaped = replace(volumeName, /'/g, "'\\''"); + let p = popen(`/usr/bin/podman volume exists '${escaped}' 2>/dev/null; echo $?`, 'r'); + let rc = p ? trim(p.read('all') || '') : ''; + if (p) p.close(); + + if (rc !== '0') { + p = popen(`/usr/bin/podman volume create '${escaped}' 2>/dev/null`, 'r'); + if (p) { p.read('all'); p.close(); } + } + + // Import via stdin pipe + p = popen(`/usr/bin/podman volume import '${escaped}' -`, 'w'); + if (!p) + die('Failed to start volume import'); + p.write(decoded); + let exit_code = p.close(); + + if (exit_code) { + // Cleanup on failure + popen(`/usr/bin/podman volume rm '${escaped}' 2>/dev/null`, 'r')?.close(); + die('Import failed'); + } +} +else { + die(`Unknown action: ${action}`); +} diff --git a/luci-app-podman/root/usr/share/luci/menu.d/luci-app-podman.json b/luci-app-podman/root/usr/share/luci/menu.d/luci-app-podman.json new file mode 100644 index 000000000..0f6c5ff6c --- /dev/null +++ b/luci-app-podman/root/usr/share/luci/menu.d/luci-app-podman.json @@ -0,0 +1,166 @@ +{ + "admin/podman": { + "title": "Podman", + "order": 60, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ + "luci-app-podman" + ] + } + }, + "admin/podman/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "podman/overview" + } + }, + "admin/podman/containers": { + "title": "Containers", + "order": 20, + "action": { + "type": "view", + "path": "podman/containers" + } + }, + "admin/podman/container/*": { + "action": { + "type": "view", + "path": "podman/container" + } + }, + "admin/podman/pods": { + "title": "Pods", + "order": 25, + "action": { + "type": "view", + "path": "podman/pods" + } + }, + "admin/podman/pod/*": { + "action": { + "type": "view", + "path": "podman/pod" + } + }, + "admin/podman/images": { + "title": "Images", + "order": 30, + "action": { + "type": "view", + "path": "podman/images" + } + }, + "admin/podman/networks": { + "title": "Networks", + "order": 40, + "action": { + "type": "view", + "path": "podman/networks" + } + }, + "admin/podman/volumes": { + "title": "Volumes", + "order": 50, + "action": { + "type": "view", + "path": "podman/volumes" + } + }, + "admin/podman/secrets": { + "title": "Secrets", + "order": 60, + "action": { + "type": "view", + "path": "podman/secrets" + } + }, + "admin/podman/stream/logs/*": { + "action": { + "type": "function", + "module": "luci.controller.podman", + "function": "container_logs" + }, + "auth": { + "methods": [ + "cookie:sysauth_https", + "cookie:sysauth_http" + ], + "login": true + } + }, + "admin/podman/stream/top/*": { + "action": { + "type": "function", + "module": "luci.controller.podman", + "function": "container_top" + }, + "auth": { + "methods": [ + "cookie:sysauth_https", + "cookie:sysauth_http" + ], + "login": true + } + }, + "admin/podman/stream/stats/*": { + "action": { + "type": "function", + "module": "luci.controller.podman", + "function": "container_stats" + }, + "auth": { + "methods": [ + "cookie:sysauth_https", + "cookie:sysauth_http" + ], + "login": true + } + }, + "admin/podman/stream/pull": { + "action": { + "type": "function", + "module": "luci.controller.podman", + "function": "image_pull" + }, + "auth": { + "methods": [ + "cookie:sysauth_https", + "cookie:sysauth_http" + ], + "login": true + } + }, + "admin/podman/stream/pod_top/*": { + "action": { + "type": "function", + "module": "luci.controller.podman", + "function": "pod_top" + }, + "auth": { + "methods": [ + "cookie:sysauth_https", + "cookie:sysauth_http" + ], + "login": true + } + }, + "admin/podman/stream/pod_stats/*": { + "action": { + "type": "function", + "module": "luci.controller.podman", + "function": "pod_stats" + }, + "auth": { + "methods": [ + "cookie:sysauth_https", + "cookie:sysauth_http" + ], + "login": true + } + } +} diff --git a/luci-app-podman/root/usr/share/podman/procd-startup-template.sh b/luci-app-podman/root/usr/share/podman/procd-startup-template.sh new file mode 100755 index 000000000..8d3f9c06e --- /dev/null +++ b/luci-app-podman/root/usr/share/podman/procd-startup-template.sh @@ -0,0 +1,62 @@ +#!/bin/sh /etc/rc.common + +# Podman's restart policy handles runtime restarts to avoid conflicts + +START={start_priority} +STOP=20 +USE_PROCD=1 + +NAME={script_name} +PROG=/usr/bin/podman + +case "{name}" in + *[!A-Za-z0-9._-]*|'') + logger -t "${NAME}" "Invalid container name '{name}'; only [A-Za-z0-9._-] allowed" + exit 1 + ;; +esac + +start_service() { + # Register procd instance - socket wait happens inside command (non-blocking) + procd_open_instance "${NAME}" + procd_set_param command /bin/sh -c " + # Wait for Podman socket with timeout (120s) + max_wait=120 + count=0 + logger -t ${NAME} 'Waiting for Podman socket...' + while [ \"\$count\" -lt \"\$max_wait\" ]; do + [ -S /run/podman/podman.sock ] && break + sleep 1 + count=\$((count + 1)) + done + if [ ! -S /run/podman/podman.sock ]; then + logger -t ${NAME} 'Timeout: Podman socket not available' + exit 1 + fi + logger -t ${NAME} \"Podman socket available after \${count}s\" + + # Check container exists + if ! $PROG container exists '{name}' 2>/dev/null; then + logger -t ${NAME} 'Container {name} does not exist' + exit 1 + fi + + # Start container if not running + if ! $PROG container inspect '{name}' --format '{{.State.Running}}' 2>/dev/null | grep -q true; then + logger -t ${NAME} 'Starting container {name}' + $PROG start '{name}' || exit 1 + else + logger -t ${NAME} 'Container {name} already running' + fi + " + # Bounded retries: if S99podman's tie-break puts it after this script, + # the socket wait above may time out on a slow first boot. A capped + # respawn gives extra 120s windows without looping forever. + procd_set_param respawn 3600 5 5 + procd_close_instance +} + +stop_service() { + logger -t ${NAME} "Stopping container {name}" + $PROG stop "{name}" 2>/dev/null || true +} diff --git a/luci-app-podman/root/usr/share/podman/pull-worker.uc b/luci-app-podman/root/usr/share/podman/pull-worker.uc new file mode 100644 index 000000000..796087c93 --- /dev/null +++ b/luci-app-podman/root/usr/share/podman/pull-worker.uc @@ -0,0 +1,102 @@ +'use strict'; + +import { open, unlink } from 'fs'; +import { urlencode, ENCODE_FULL } from 'lucihttp'; // ucode-lsp disable +import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable +import { API_BASE } from 'luci.podman_socket'; // ucode-lsp disable +import { build_request, parse_status, read_headers } from 'luci.podman_http'; // ucode-lsp disable + +const BLOCKSIZE = 4096; + +const reference = ARGV[0]; +const logfile = ARGV[1]; +const pidfile = ARGV[2]; + +/** + * @param {string} msg + */ +function write_error(msg) { + let f = open(logfile, 'a'); + if (f) { + f.write('{"error":"' + replace(replace(msg, /\\/g, '\\\\'), /"/g, '\\"') + '"}\n'); + f.flush(); + f.close(); + } +} + +/** + * @param {int} code + */ +function cleanup(code) { + unlink(pidfile); + exit(code ?? 0); +} + +if (!reference || !logfile || !pidfile) + exit(1); + +// Write own PID to pidfile so the streaming endpoint can check if we are running +let ps = open('/proc/self/stat', 'r'); +if (ps) { + let pid = +(split(ps.read(200) || '0', ' ')[0]); + ps.close(); + let pf = open(pidfile, 'w'); + if (pf) { pf.write(pid + '\n'); pf.close(); } +} + +// Connect to Podman socket +let sock = podman_socket.connect(); +if (!sock) { + write_error('Cannot connect to Podman socket'); + cleanup(1); +} + +let encoded = urlencode(reference, ENCODE_FULL); + +sock.send(build_request( + 'POST', + sprintf('%s/images/pull?reference=%s&quiet=false', API_BASE, encoded), + null +)); + +// Read HTTP response headers (blocking recv - no uloop needed in standalone process) +let lf = null; + +let hdrs = read_headers(sock, BLOCKSIZE); +if (!hdrs) { + write_error('Podman closed connection before responding'); + sock.close(); + cleanup(1); +} + +let code = parse_status(hdrs.header_buf) || 502; +let body = hdrs.body_remainder; + +if (code !== 200) { + let parsed = null; + try { parsed = json(body); } catch(e) {} + let msg = (parsed?.message) || (parsed?.cause) + || replace(body, /\s+$/, '') + || sprintf('Podman error %d', code); + write_error(msg); + sock.close(); + cleanup(1); +} + +lf = open(logfile, 'a'); +if (!lf) { sock.close(); cleanup(1); } + +// Write any body bytes that arrived together with headers +if (length(body)) { lf.write(body); lf.flush(); } + +// Stream response body to logfile - Podman sends NDJSON, we write it as-is +while (true) { + let chunk = sock.recv(BLOCKSIZE); + if (!chunk) break; // EOF or error - Podman finished + lf.write(chunk); + lf.flush(); +} + +lf.close(); +sock.close(); +cleanup(0); diff --git a/luci-app-podman/root/usr/share/rpcd/acl.d/luci-app-podman.json b/luci-app-podman/root/usr/share/rpcd/acl.d/luci-app-podman.json new file mode 100644 index 000000000..f7aa1c39d --- /dev/null +++ b/luci-app-podman/root/usr/share/rpcd/acl.d/luci-app-podman.json @@ -0,0 +1,101 @@ +{ + "luci-app-podman": { + "description": "Grant access to Podman RPC methods", + "read": { + "cgi-io": [ + "exec" + ], + "file": { + "/usr/libexec/podman-api volume_export *": [ + "exec" + ] + }, + "ubus": { + "podman": [ + "containers_list", + "container_inspect", + "container_stats", + "container_top", + "container_healthcheck_run", + "images_list", + "image_inspect", + "image_manifest_inspect", + "networks_list", + "network_inspect", + "volumes_list", + "volume_inspect", + "pods_list", + "pod_inspect", + "secrets_list", + "secret_inspect", + "system_df", + "version", + "info", + "init_script_status", + "init_script_show", + "system_debug" + ] + }, + "uci": [ + "luci-podman", + "firewall", + "network", + "dhcp" + ] + }, + "write": { + "cgi-io": [ + "exec", + "upload" + ], + "file": { + "/tmp/podman-import": [ + "write" + ] + }, + "ubus": { + "podman": [ + "container_start", + "container_stop", + "container_restart", + "container_pause", + "container_unpause", + "container_remove", + "container_create", + "container_rename", + "container_update", + "container_healthcheck_run", + "image_remove", + "image_pull", + "network_create", + "network_remove", + "network_connect", + "network_disconnect", + "volume_create", + "volume_remove", + "volume_import", + "pod_start", + "pod_stop", + "pod_kill", + "pod_restart", + "pod_pause", + "pod_unpause", + "pod_remove", + "pod_create", + "secret_create", + "secret_remove", + "system_prune", + "init_script_generate", + "init_script_set_enabled", + "init_script_remove" + ] + }, + "uci": [ + "luci-podman", + "firewall", + "network", + "dhcp" + ] + } + } +} diff --git a/luci-app-podman/root/usr/share/rpcd/ucode/podman.uc b/luci-app-podman/root/usr/share/rpcd/ucode/podman.uc new file mode 100644 index 000000000..7d844edc1 --- /dev/null +++ b/luci-app-podman/root/usr/share/rpcd/ucode/podman.uc @@ -0,0 +1,987 @@ +'use strict'; + +// --- Table of Contents --- +// +// Infrastructure (lines 30-225) +// Configuration, validation, HTTP client, helpers +// +// RPC Methods (line 228+) +// Containers: list inspect start stop restart remove stats create rename +// update healthcheck top +// Images: list inspect remove manifest_inspect pull +// Networks: list inspect remove create connect disconnect +// Volumes: list inspect remove create import +// Pods: list inspect start stop restart pause unpause remove create stats +// Secrets: list inspect create remove +// System: df prune version info debug +// Init Scripts: generate show status set_enabled remove +// (exempt from socket check - operate on /etc/init.d/ only) +// +// Socket wrapper (end of file) + +import { readfile, writefile, popen, stat, chmod, unlink, glob, access } from 'fs'; +import { cursor } from 'uci'; +import { urlencode, ENCODE_FULL } from 'lucihttp'; // ucode-lsp disable +import { init_enabled, init_action } from 'luci.sys'; // ucode-lsp disable +import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable +import { API_BASE } from 'luci.podman_socket'; // ucode-lsp disable +import { + validate_id, validate_name, validate_image_ref, validate_query_params, + validate_restart_policy, require_param, BODY_KEYS +} from 'luci.podman_validate'; // ucode-lsp disable +import { + build_request, parse_status, parse_content_length, read_headers +} from 'luci.podman_http'; // ucode-lsp disable + +// --- Configuration --- + +const uci = cursor(); +const _prio = uci.get('luci-podman', 'globals', 'init_start_priority'); +// Capped at 99: procd sorts rc.d symlinks as plain strings, so a 3-digit +// value like 100 sorts *before* 2-digit priorities (e.g. network at 19-20). +const INIT_START_PRIORITY = (type(_prio) === 'string' && match(_prio, /^([0-9]|[1-9][0-9])$/)) ? _prio : '99'; +uci.unload('luci-podman'); + +// "podman" is a strict string prefix of this, so generated init scripts +// always sort after the real /etc/init.d/podman at any tied START value. +const INIT_SCRIPT_PREFIX = 'podman-container-'; + +// Validators come from luci.podman_validate (imported above). + +// --- HTTP Client --- + +/** + * @param {string} method + * @param {string} path + * @param {string} body + * @param {bool} raw + */ +function podman_request(method, path, body, raw) { + let sock = podman_socket.connect(); + if (!sock) + return { error: 'Failed to connect to Podman socket' }; + + sock.send(build_request(method, path, body)); + + let hdrs = read_headers(sock); + if (!hdrs) { + sock.close(); + return { error: 'Empty response from Podman API' }; + } + let header_buf = hdrs.header_buf; + let body_remainder = hdrs.body_remainder; + let chunk = ''; + + let status_code = parse_status(header_buf); + + // For 204 No Content (success with no body - e.g., start/stop/restart) + if (status_code === 204) { + sock.close(); + if (raw) return { status: 204, body: '' }; + return {}; + } + + let content_length = parse_content_length(header_buf); + + // Read body + let resp_body = `${body_remainder}`; + if (content_length >= 0) { + // Read exactly content_length bytes + while (length(resp_body) < content_length) { + chunk = sock.recv(65536); + if (!chunk) + break; + resp_body += `${chunk}`; + } + } else { + // No Content-Length - read until EOF + while (true) { + chunk = sock.recv(65536); + if (!chunk) + break; + resp_body += `${chunk}`; + } + } + + sock.close(); + + // Raw mode: return status + body without JSON parsing + if (raw) { + if (status_code >= 400) + return { error: trim(resp_body || `HTTP ${status_code}`) }; + return { status: status_code, body: resp_body }; + } + + // Try to parse JSON body + if (resp_body !== '') { + let parsed = null; + try { parsed = json(resp_body); } catch(e) {} + + if (parsed != null) { + // Wrap arrays in { data: [...] } for frontend compatibility + if (type(parsed) === 'array') + return { data: parsed }; + return parsed; + } + // Non-JSON response (e.g., plain text error) + if (status_code >= 400) + return { error: trim(resp_body) }; + return { data: resp_body }; + } + + if (status_code >= 400) + return { error: `HTTP ${status_code}` }; + + return {}; +} + +/** + * @param {string} id + */ +function encode_id(id) { + return urlencode(id, ENCODE_FULL); +} + + +/** + * @param {object} params + */ +function build_bool_query(params) { + let parts = []; + for (let k in params) { + if (params[k] === true || params[k] === 1) + push(parts, `${k}=true`); + } + return length(parts) ? '?' + join('&', parts) : ''; +} + +/** @param {any} data */ +function to_json_body(data) { + return (type(data) === 'string') ? data : sprintf('%J', data); +} + +// --- Init Script Helpers --- + +/** + * @param {string} name + */ +function init_script_path(name) { + return `/etc/init.d/${INIT_SCRIPT_PREFIX}${name}`; +} + +// --- RPC Methods --- + +const methods = { + // ==================== Containers ==================== + + containers_list: { + args: { query: '' }, + call: function(req) { + let path = `${API_BASE}/containers/json`; + if (req.args.query && req.args.query !== '') { + let err = validate_query_params(req.args.query); + if (err) return { error: err }; + path += `?${req.args.query}`; + } + return podman_request('GET', path); + } + }, + + container_inspect: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/containers/${encode_id(req.args.id)}/json`); + } + }, + + container_start: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/start`); + } + }, + + container_stop: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/stop`); + } + }, + + container_restart: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/restart`); + } + }, + + container_pause: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/pause`); + } + }, + + container_unpause: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/unpause`); + } + }, + + container_remove: { + args: { id: '', force: false, depend: false }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + let query = build_bool_query({ force: req.args.force, depend: req.args.depend }); + return podman_request('DELETE', `${API_BASE}/containers/${encode_id(req.args.id)}${query}`); + } + }, + + container_stats: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/containers/${encode_id(req.args.id)}/stats?stream=false`); + } + }, + + container_create: { + args: { data: {} }, + call: function(req) { + let data = req.args.data; + let err = require_param('data', data); + if (err) return { error: err }; + let body = to_json_body(data); + return podman_request('POST', `${API_BASE}/containers/create`, body); + } + }, + + container_rename: { + args: { id: '', name: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id) + || require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + let name_enc = encode_id(req.args.name); + return podman_request('POST', `${API_BASE}/containers/${encode_id(req.args.id)}/rename?name=${name_enc}`); + } + }, + + container_update: { + args: { id: '', data: {} }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id) + || require_param('data', req.args.data); + if (err) return { error: err }; + + let data = req.args.data; + if (type(data) === 'string') + data = json(data); + if (!data) + return { error: 'Invalid JSON data' }; + + let id_enc = encode_id(req.args.id); + + // Build query params for restart policy + let query_parts = []; + if (data.RestartPolicy) { + let perr = validate_restart_policy(data.RestartPolicy); + if (perr) return { error: perr }; + push(query_parts, `restartPolicy=${data.RestartPolicy}`); + } + if (data.RestartRetries != null) + push(query_parts, `restartRetries=${+data.RestartRetries | 0}`); + + let query = length(query_parts) ? '?' + join('&', query_parts) : ''; + + // Determine if body is needed (resource/health updates) + let body_str = sprintf('%J', data); + let has_body_fields = false; + for (let k in data) { + if (k in BODY_KEYS) { has_body_fields = true; break; } + } + + return podman_request('POST', `${API_BASE}/containers/${id_enc}/update${query}`, + has_body_fields ? body_str : '{}'); + } + }, + + container_healthcheck_run: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/containers/${encode_id(req.args.id)}/healthcheck`); + } + }, + + container_top: { + args: { id: '', ps_args: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_id(req.args.id); + if (err) return { error: err }; + let path = `${API_BASE}/containers/${encode_id(req.args.id)}/top`; + if (req.args.ps_args && req.args.ps_args !== '') + path += `?ps_args=${encode_id(req.args.ps_args)}`; + return podman_request('GET', path); + } + }, + + // ==================== Images ==================== + + images_list: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/images/json`); + } + }, + + image_inspect: { + args: { id: '' }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_image_ref(req.args.id); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/images/${encode_id(req.args.id)}/json`); + } + }, + + image_remove: { + args: { id: '', force: false }, + call: function(req) { + let err = require_param('id', req.args.id) || validate_image_ref(req.args.id); + if (err) return { error: err }; + return podman_request('DELETE', `${API_BASE}/images/${encode_id(req.args.id)}${build_bool_query({ force: req.args.force })}`); + } + }, + + image_manifest_inspect: { + args: { image: '' }, + call: function(req) { + let err = require_param('image', req.args.image) || validate_image_ref(req.args.image); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/manifests/${encode_id(req.args.image)}/json`); + } + }, + + image_pull: { + args: { image: '' }, + call: function(req) { + let err = require_param('image', req.args.image) || validate_image_ref(req.args.image); + if (err) return { error: err }; + + let image_enc = urlencode(req.args.image, ENCODE_FULL); + let resp = podman_request('POST', `${API_BASE}/images/pull?reference=${image_enc}`, null, true); + if (resp.error) return resp; + + // Response is newline-delimited JSON: {"stream":"..."} lines + // Last line contains {"images":[...],"id":"..."} + let body = `${resp.body || ''}`; + let output = ''; + let images = null; + let id = null; + + let lines = split(body, '\n'); + for (let i = 0; i < length(lines); i++) { + let raw = lines[i]; + if (type(raw) !== 'string') continue; + let line = trim(raw); + if (!line) continue; + let parsed = null; + try { parsed = json(line); } catch(e) {} + if (!parsed) continue; + + if (parsed.stream) + output += parsed.stream; + if (parsed.images) + images = parsed.images; + if (parsed.id) + id = parsed.id; + if (parsed.error) + return { error: parsed.error }; + } + + return { output: output, images: images, id: id }; + } + }, + + // ==================== Networks ==================== + + networks_list: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/networks/json`); + } + }, + + network_inspect: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/networks/${encode_id(req.args.name)}/json`); + } + }, + + network_remove: { + args: { name: '', force: false }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('DELETE', `${API_BASE}/networks/${encode_id(req.args.name)}${build_bool_query({ force: req.args.force })}`); + } + }, + + network_create: { + args: { data: {} }, + call: function(req) { + let data = req.args.data; + let err = require_param('data', data); + if (err) return { error: err }; + let body = to_json_body(data); + return podman_request('POST', `${API_BASE}/networks/create`, body); + } + }, + + network_connect: { + args: { name: '', data: {} }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name) + || require_param('data', req.args.data); + if (err) return { error: err }; + let body = to_json_body(req.args.data); + return podman_request('POST', `${API_BASE}/networks/${encode_id(req.args.name)}/connect`, body); + } + }, + + network_disconnect: { + args: { name: '', data: {} }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name) + || require_param('data', req.args.data); + if (err) return { error: err }; + let body = to_json_body(req.args.data); + return podman_request('POST', `${API_BASE}/networks/${encode_id(req.args.name)}/disconnect`, body); + } + }, + + // ==================== Volumes ==================== + + volumes_list: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/volumes/json`); + } + }, + + volume_inspect: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/volumes/${encode_id(req.args.name)}/json`); + } + }, + + volume_remove: { + args: { name: '', force: false }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('DELETE', `${API_BASE}/volumes/${encode_id(req.args.name)}${build_bool_query({ force: req.args.force })}`); + } + }, + + volume_create: { + args: { data: {} }, + call: function(req) { + let data = req.args.data; + let err = require_param('data', data); + if (err) return { error: err }; + let body = to_json_body(data); + return podman_request('POST', `${API_BASE}/volumes/create`, body); + } + }, + + volume_import: { + args: { name: '', compressed: false }, + call: function(req) { + let name = req.args.name; + let err = require_param('name', name) || validate_name(name); + if (err) return { error: err }; + + let filepath = '/tmp/podman-import'; + if (!stat(filepath)) + return { error: 'Upload file not found' }; + + let existing = podman_request('GET', `${API_BASE}/volumes/${encode_id(name)}/json`); + if (!existing.Name) { + let created = podman_request('POST', `${API_BASE}/volumes/create`, + sprintf('%J', { Name: name })); + if (created.error) return created; + } + + let cmd; + if (req.args.compressed === true || req.args.compressed === 1) + cmd = sprintf('gunzip -c "%s" | /usr/bin/podman volume import "%s" - 2>&1', filepath, name); + else + cmd = sprintf('/usr/bin/podman volume import "%s" "%s" 2>&1', name, filepath); + + let p = popen(cmd, 'r'); + let output = p ? trim(p.read('all') || '') : ''; + let exit_code = p ? p.close() : 1; + + unlink(filepath); + + if (exit_code) + return { error: output || 'Import failed' }; + + return {}; + } + }, + + // ==================== Pods ==================== + + pods_list: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/pods/json`); + } + }, + + pod_inspect: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/pods/${encode_id(req.args.name)}/json`); + } + }, + + pod_start: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/start`); + } + }, + + pod_stop: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/stop`); + } + }, + + pod_kill: { + args: { name: '', signal: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + let sig = req.args.signal || 'SIGKILL'; + return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/kill?signal=${sig}`); + } + }, + + pod_restart: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/restart`); + } + }, + + pod_pause: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/pause`); + } + }, + + pod_unpause: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('POST', `${API_BASE}/pods/${encode_id(req.args.name)}/unpause`); + } + }, + + pod_remove: { + args: { name: '', force: false }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('DELETE', `${API_BASE}/pods/${encode_id(req.args.name)}${build_bool_query({ force: req.args.force })}`); + } + }, + + pod_create: { + args: { data: {} }, + call: function(req) { + let data = req.args.data; + let err = require_param('data', data); + if (err) return { error: err }; + let body = to_json_body(data); + return podman_request('POST', `${API_BASE}/pods/create`, body); + } + }, + + // ==================== Secrets ==================== + + secrets_list: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/secrets/json`); + } + }, + + secret_inspect: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('GET', `${API_BASE}/secrets/${encode_id(req.args.name)}/json`); + } + }, + + secret_create: { + args: { name: '', data: '', labels: {} }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name) + || require_param('data', req.args.data); + if (err) return { error: err }; + let query = `?name=${encode_id(req.args.name)}`; + let labels = req.args.labels; + if (type(labels) === 'object' && length(labels) > 0) + query += `&labels=${urlencode(sprintf('%J', labels), ENCODE_FULL)}`; + return podman_request('POST', `${API_BASE}/secrets/create${query}`, `${req.args.data}`); + } + }, + + secret_remove: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + return podman_request('DELETE', `${API_BASE}/secrets/${encode_id(req.args.name)}`); + } + }, + + // ==================== System ==================== + + system_df: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/system/df`); + } + }, + + system_prune: { + args: { all: false, volumes: false }, + call: function(req) { + let query = build_bool_query({ all: req.args.all, volumes: req.args.volumes }); + return podman_request('POST', `${API_BASE}/system/prune${query}`); + } + }, + + version: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/version`); + } + }, + + info: { + args: {}, + call: function() { + return podman_request('GET', `${API_BASE}/info`); + } + }, + + // ==================== System Debug ==================== + + system_debug: { + args: {}, + call: function() { + let checks = []; + + // 1. Podman binary + let podman_stat = stat('/usr/bin/podman'); + if (podman_stat) { + let proc = popen('/usr/bin/podman --version 2>/dev/null', 'r'); + let ver = proc ? trim(proc.read('line') || '') : ''; + if (proc) proc.close(); + push(checks, { name: 'podman_binary', label: 'Podman Binary', status: 'ok', detail: '/usr/bin/podman', message: ver }); + } else { + push(checks, { name: 'podman_binary', label: 'Podman Binary', status: 'error', detail: '/usr/bin/podman', message: 'Not found or not executable' }); + } + + // 2. Podman socket reachable + let dest = podman_socket.get_dest(); + let local_path = podman_socket.get_local_path(); + let socket_stat = null; + if (!local_path) { + push(checks, { name: 'podman_socket', label: 'Podman Socket', status: 'ok', detail: dest, message: 'Remote TCP (cannot stat)' }); + } else { + socket_stat = stat(local_path); + if (socket_stat && socket_stat.type === 'socket') { + push(checks, { name: 'podman_socket', label: 'Podman Socket', status: 'ok', detail: dest, message: 'Socket file exists' }); + } else { + push(checks, { name: 'podman_socket', label: 'Podman Socket', status: 'error', detail: dest, message: 'Socket not found' }); + } + } + + // 3. Socket responsive + if (podman_socket.is_remote() || (socket_stat && socket_stat.type === 'socket')) { + let ping = podman_request('GET', `${API_BASE}/_ping`); + if (ping && ping.data === 'OK') { + push(checks, { name: 'socket_responsive', label: 'Socket Responsive', status: 'ok', detail: 'API responding', message: '' }); + } else { + push(checks, { name: 'socket_responsive', label: 'Socket Responsive', status: 'warn', detail: 'API not responding', message: ping ? (ping.error || '') : '' }); + } + } else { + push(checks, { name: 'socket_responsive', label: 'Socket Responsive', status: 'error', detail: 'Skipped', message: 'Socket not available' }); + } + + // 4. ucode-mod-socket + push(checks, { name: 'ucode_socket', label: 'ucode-mod-socket', status: 'ok', detail: 'Built-in', message: 'Available (running ucode)' }); + + // 5. Init directory writable + let init_stat = stat('/etc/init.d'); + if (init_stat && init_stat.type === 'directory') { + // Test writability by checking we can create a temp file + let test_path = '/etc/init.d/.podman_write_test'; + let ok = writefile(test_path, ''); + if (ok != null) { + unlink(test_path); + push(checks, { name: 'init_dir_writable', label: 'Init Directory', status: 'ok', detail: '/etc/init.d/', message: 'Writable' }); + } else { + push(checks, { name: 'init_dir_writable', label: 'Init Directory', status: 'warn', detail: '/etc/init.d/', message: 'Not writable - init scripts cannot be created' }); + } + } else { + push(checks, { name: 'init_dir_writable', label: 'Init Directory', status: 'error', detail: '/etc/init.d/', message: 'Directory not found' }); + } + + // 6. Startup template + let template = '/usr/share/podman/procd-startup-template.sh'; + let tmpl_stat = stat(template); + if (tmpl_stat) { + let content = readfile(template); + if (content != null) { + push(checks, { name: 'startup_template', label: 'Startup Template', status: 'ok', detail: template, message: 'Readable' }); + } else { + push(checks, { name: 'startup_template', label: 'Startup Template', status: 'warn', detail: template, message: 'Exists but not readable' }); + } + } else { + push(checks, { name: 'startup_template', label: 'Startup Template', status: 'warn', detail: template, message: 'Not found - init script generation will fail' }); + } + + // 7. RPC plugin (self-check - if we're running, we exist) + push(checks, { name: 'rpc_plugin', label: 'RPC Plugin', status: 'ok', detail: '/usr/share/rpcd/ucode/podman.uc', message: 'Running (ucode)' }); + + // 8. Podman API helper + let api_helper = '/usr/libexec/podman-api'; + let helper_stat = stat(api_helper); + if (access(api_helper, 'x')) { + push(checks, { name: 'podman_api_helper', label: 'Podman API Helper', status: 'ok', detail: api_helper, message: 'Executable' }); + } else if (stat(api_helper)) { + push(checks, { name: 'podman_api_helper', label: 'Podman API Helper', status: 'warn', detail: api_helper, message: 'Exists but not executable' }); + } else { + push(checks, { name: 'podman_api_helper', label: 'Podman API Helper', status: 'warn', detail: api_helper, message: 'Not found - volume export/import will fail' }); + } + + // 9. UCI config + let uci_ctx = cursor(); + let socket_cfg = uci_ctx.get('luci-podman', 'globals', 'socket_path'); + let globals_ok = uci_ctx.get('luci-podman', 'globals'); + uci_ctx.unload('luci-podman'); + if (globals_ok) { + push(checks, { name: 'uci_config', label: 'UCI Config', status: 'ok', detail: 'luci-podman.globals', message: `socket_path=${socket_cfg || 'default'}` }); + } else { + push(checks, { name: 'uci_config', label: 'UCI Config', status: 'warn', detail: '/etc/config/luci-podman', message: 'Config not found or not loadable' }); + } + + // 10. Containers config + let containers_conf = '/etc/containers/containers.conf'; + let cc_stat = stat(containers_conf); + if (cc_stat) { + push(checks, { name: 'containers_conf', label: 'Containers Config', status: 'ok', detail: containers_conf, message: 'Readable' }); + } else { + push(checks, { name: 'containers_conf', label: 'Containers Config', status: 'warn', detail: containers_conf, message: 'Not found - using Podman defaults' }); + } + + // 11. Network config directory + let net_dir = '/etc/containers/networks'; + let nd_stat = stat(net_dir); + if (nd_stat && nd_stat.type === 'directory') { + let count = length(glob('/etc/containers/networks/*.json') ?? []); + push(checks, { name: 'network_config_dir', label: 'Network Config Dir', status: 'ok', detail: net_dir, message: `${count} network(s)` }); + } else { + push(checks, { name: 'network_config_dir', label: 'Network Config Dir', status: 'warn', detail: net_dir, message: 'Not found' }); + } + + return { checks: checks }; + } + }, + + // ==================== Init Scripts ==================== + + init_script_generate: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + + let name = `${req.args.name}`; + let start_priority = `${INIT_START_PRIORITY}`; + let script_name = `${INIT_SCRIPT_PREFIX}${name}`; + let script_path = init_script_path(name); + + let template = readfile('/usr/share/podman/procd-startup-template.sh'); + if (!template) + return { error: 'Failed to read startup template' }; + + let content = replace(template, /\{name\}/g, name); + content = replace(content, /\{start_priority\}/g, start_priority); + content = replace(content, /\{script_name\}/g, script_name); + + let written = writefile(script_path, content); + if (written == null) + return { error: 'Failed to create init script' }; + + chmod(script_path, 0755); + + return { success: true, path: script_path }; + } + }, + + init_script_show: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + + let path = init_script_path(req.args.name); + let content = readfile(path); + if (content == null) + return { error: 'Init script not found' }; + + return { content: content, path: path }; + } + }, + + init_script_status: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + + let script_path = init_script_path(req.args.name); + let exists = !!stat(script_path); + let enabled = false; + + if (exists) + enabled = init_enabled(`${INIT_SCRIPT_PREFIX}${req.args.name}`); + + return { exists: exists, enabled: enabled }; + } + }, + + init_script_set_enabled: { + args: { name: '', enabled: false }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + + let script_path = init_script_path(req.args.name); + if (!stat(script_path)) + return { error: 'Init script not found. Generate it first.' }; + + let action = (req.args.enabled === true || req.args.enabled === 1) ? 'enable' : 'disable'; + let rc = init_action(`${INIT_SCRIPT_PREFIX}${req.args.name}`, action); + if (rc) + return { error: `Failed to ${action} service` }; + + return { success: true, enabled: (action === 'enable') }; + } + }, + + init_script_remove: { + args: { name: '' }, + call: function(req) { + let err = require_param('name', req.args.name) || validate_name(req.args.name); + if (err) return { error: err }; + + let script_path = init_script_path(req.args.name); + if (!stat(script_path)) + return { success: true, message: 'Init script does not exist' }; + + // Disable before removing + init_action(`${INIT_SCRIPT_PREFIX}${req.args.name}`, 'disable'); + unlink(script_path); + + if (stat(script_path)) + return { error: 'Failed to remove init script' }; + + return { success: true }; + } + } +}; + +// --- Socket check wrapper --- +// Wrap all methods except system_debug with a socket availability check + +const no_socket_check = { + system_debug: true, + init_script_generate: true, + init_script_show: true, + init_script_status: true, + init_script_set_enabled: true, + init_script_remove: true +}; + +const _local_socket_path = podman_socket.get_local_path(); + +const wrapped_methods = {}; +for (let name in methods) { + let method = methods[name]; + if (no_socket_check[name]) { + wrapped_methods[name] = method; + } else { + wrapped_methods[name] = { + args: method.args, + call: function(req) { + // Cheap pre-check for unix sockets only; TCP relies on actual connect failures. + if (_local_socket_path) { + let s = stat(_local_socket_path); + if (!s || s.type !== 'socket') + return { error: 'Podman socket not found or not accessible' }; + } + return method.call(req); + } + }; + } +} + +return { 'podman': wrapped_methods }; diff --git a/luci-app-podman/ucode/controller/podman.uc b/luci-app-podman/ucode/controller/podman.uc new file mode 100644 index 000000000..ec36bd05f --- /dev/null +++ b/luci-app-podman/ucode/controller/podman.uc @@ -0,0 +1,479 @@ +'use strict'; + +import { open, stat, unlink } from 'fs'; +import * as uloop from 'uloop'; +import * as struct from 'struct'; +import { cursor } from 'uci'; +import { connect as ubus_connect } from 'ubus'; +import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable +import { API_BASE } from 'luci.podman_socket'; // ucode-lsp disable +import { validate_id } from 'luci.podman_validate'; // ucode-lsp disable +import { build_request, parse_status } from 'luci.podman_http'; // ucode-lsp disable + +const BLOCKSIZE = 4096; + +const FLUSH_SIZE = 1500; +const PAD_STR = (() => { + let s = ''; let i = 0; + while (i < FLUSH_SIZE) { s += ' '; i++; } + return s; +})(); + +/** @param {string} data */ +function http_write_padded(data) { + let n = length(data); + if (n >= FLUSH_SIZE) + return http.write(data); + return http.write(data + substr(PAD_STR, 0, FLUSH_SIZE - n)); // ucode-lsp disable +} + +const _uci_timeouts = (() => { + let c = cursor(); + let s = c.get('uhttpd', 'main', 'script_timeout'); + let n = c.get('uhttpd', 'main', 'network_timeout'); + c.unload('uhttpd'); + return { script: s ? +s : 60, network: n ? +n : 30 }; +})(); + + +/** + * @param {int} code + * @param {string} message + */ +function error_response(code, message) { + if (http.eoh) return; // ucode-lsp disable + http.status(code, message); + http.header('Content-Type', 'text/plain'); + http.write(message + '\n'); +} + + +/** + * @param {string} sid + */ +function session_timer(sid) { + let uci = cursor(); + let script_timeout = +(uci.get('uhttpd', 'main', 'script_timeout') ?? 60); + let sessiontime = +(uci.get('luci', 'sauth', 'sessiontime') ?? 3600); + let timeout_ms = (script_timeout - 5) * 1000; + + if (!sid) + return { timeout_ms, is_final: false, on_expire: () => uloop.end() }; + + let uconn = ubus_connect(); + let now = time(); + + let sdat = uconn.call('session', 'get', { ubus_rpc_session: sid }); + let deadline = +(sdat?.values?.podman_stream_deadline ?? 0); + let last_seen = +(sdat?.values?.podman_stream_last_seen ?? 0); + + let is_fresh = !deadline || (now - last_seen > script_timeout); + + if (is_fresh) + deadline = now + sessiontime; + + uconn.call('session', 'set', { + ubus_rpc_session: sid, + values: { podman_stream_deadline: deadline, podman_stream_last_seen: now } + }); + + let remaining_ms = (deadline - now) * 1000; + + if (remaining_ms <= 0) { + uconn.call('session', 'destroy', { ubus_rpc_session: sid }); + return null; + } + + let is_final = remaining_ms <= timeout_ms; + let fire_ms = is_final ? remaining_ms : timeout_ms; + + return { + timeout_ms: fire_ms, + is_final, + on_expire: () => { + if (is_final) + uconn.call('session', 'destroy', { ubus_rpc_session: sid }); + uloop.end(); + } + }; +} + +/** + * @param {string} api_path + * @param {function} on_data + * @param {boolean} early_headers + * @param {int} timer + */ +function stream_podman(api_path, on_data, early_headers, timer) { + let sock = podman_socket.connect(); + if (!sock) { + error_response(502, 'Cannot connect to Podman socket'); + return; + } + + sock.send(build_request('GET', api_path, null)); + + let t = _uci_timeouts; + let ka_ms = (t.network - 5) * 1000; + + uloop.init(); + + let response_started = early_headers; + if (early_headers) { + http.status(200, 'OK'); + http.header('Content-Type', 'text/plain'); + http.write_headers(); + http.write('\n'); + } + + let buf = ''; + let podman_headers_done = false; + + let handle = uloop.handle(sock, () => { + let chunk = sock.recv(BLOCKSIZE); + if (type(chunk) !== 'string') return; // EAGAIN - keep waiting + + if (!podman_headers_done) { + // Phase 1: consuming Podman's HTTP response headers + if (!length(chunk)) { + // EOF before Podman sent headers + if (!response_started) + error_response(502, 'Podman closed connection'); + uloop.end(); + return; + } + buf += chunk; + if (type(buf) !== 'string') return; + let sep = index(buf, '\r\n\r\n'); + if (type(sep) !== 'int' || sep < 0) return; // headers not complete yet + + let code = parse_status(buf) || 502; + if (code != 200) { + if (!response_started) + error_response(code, substr(buf, sep + 4) || sprintf('Podman error %d', code)); + uloop.end(); + return; + } + + sep = sep + 4; + let body_start = substr(buf, sep); + buf = ''; + podman_headers_done = true; + + if (!response_started) { + // Stats: start our response now that Podman confirmed 200 + http.status(200, 'OK'); + http.header('Content-Type', 'application/x-ndjson'); + http.write_headers(); + http.write('\n'); + response_started = true; + } + + if (body_start && length(body_start)) { + if (!on_data(body_start)) { uloop.end(); return; } + } + } else { + // Phase 2: streaming data + if (!length(chunk)) { uloop.end(); return; } // Podman closed stream + if (!on_data(chunk)) uloop.end(); + } + }, uloop.ULOOP_READ); + + if (early_headers) + uloop.timer(ka_ms, () => http_write_padded('\n')); + + let effective_timeout = early_headers ? min(timer.timeout_ms, ka_ms * 2) : timer.timeout_ms; + uloop.timer(effective_timeout, timer.on_expire); + + uloop.run(); + + handle.delete(); + sock.close(); + + if (!response_started) + error_response(504, 'Podman did not respond'); +} + +return { + container_logs: (id) => { + if (validate_id(id)) { error_response(400, 'Invalid container ID'); return; } + + let timer = session_timer(ctx?.authsession); // ucode-lsp disable + if (!timer) { error_response(403, 'Session expired'); return; } + + let tail = http.formvalue('tail') || '100'; + let since = http.formvalue('since') || ''; + let until = http.formvalue('until') || ''; + let follow = http.formvalue('follow') !== 'false'; + + if (tail !== 'all' && !match(`${tail}`, /^[0-9]+$/)) { + error_response(400, 'Invalid tail parameter'); + return; + } + + if (since && !match(`${since}`, /^[0-9]+(\.[0-9]+)?$/)) { + error_response(400, 'Invalid since parameter'); + return; + } + + if (until && !match(`${until}`, /^[0-9]+(\.[0-9]+)?$/)) { + error_response(400, 'Invalid until parameter'); + return; + } + + let api_path = sprintf( + '%s/containers/%s/logs?follow=%s&stdout=true&stderr=true×tamps=false', + API_BASE, id, follow ? 'true' : 'false' + ); + if (tail) api_path += sprintf('&tail=%s', tail); + if (since) api_path += sprintf('&since=%s', since); + if (until) api_path += sprintf('&until=%s', until); + + let framebuf = ''; + + stream_podman(api_path, (chunk) => { + framebuf += `${chunk}`; + let out = ''; + while (length(framebuf) >= 8) { + let hdr = struct.unpack('!BxxxI', substr(framebuf, 0, 8)); + if (!hdr) break; + let stream_type = hdr[0]; + let payload_len = int(`${hdr[1]}`); + if (length(framebuf) < 8 + payload_len) break; // wait for more data + let payload = substr(framebuf, 8, payload_len); + framebuf = substr(framebuf, 8 + payload_len); + // Emit stdout (1) and stderr (2); skip others + if (stream_type >= 1 && stream_type <= 2) + out += payload; + } + if (length(out)) http_write_padded(out); + return true; + }, true, timer); + }, + + container_top: (id) => { + if (validate_id(id)) { error_response(400, 'Invalid container ID'); return; } + + let timer = session_timer(ctx?.authsession); // ucode-lsp disable + if (!timer) { error_response(403, 'Session expired'); return; } + + let delay = http.formvalue('delay') || '5'; + let ps_args = http.formvalue('ps_args') || ''; + + if (!match(`${delay}`, /^[0-9]+$/) || +delay < 2) { + error_response(400, 'Invalid delay parameter'); + return; + } + + if (ps_args && !match(`${ps_args}`, /^[-a-zA-Z0-9_, ]+$/)) { + error_response(400, 'Invalid ps_args parameter'); + return; + } + + let api_path = sprintf('%s/containers/%s/top?stream=true&delay=%s', API_BASE, id, delay); + + if (ps_args) + api_path += sprintf('&ps_args=%s', replace(ps_args, / /g, '%20')); + + stream_podman(api_path, (chunk) => { + return http_write_padded(chunk); + }, false, timer); + }, + + container_stats: (id) => { + if (validate_id(id)) { error_response(400, 'Invalid container ID'); return; } + + let timer = session_timer(ctx?.authsession); // ucode-lsp disable + if (!timer) { error_response(403, 'Session expired'); return; } + + let interval = `${http.formvalue('interval') || '3'}`; + if (!match(interval, /^[0-9]+$/) || +interval < 1) { + error_response(400, 'Invalid interval parameter'); + return; + } + let api_path = sprintf('%s/containers/stats?containers=%s&stream=true&interval=%s', + API_BASE, id, interval); + + stream_podman(api_path, (chunk) => { + return http_write_padded(chunk); + }, false, timer); + }, + + pod_top: (name) => { + if (validate_id(name)) { error_response(400, 'Invalid pod name or ID'); return; } + + let timer = session_timer(ctx?.authsession); // ucode-lsp disable + if (!timer) { error_response(403, 'Session expired'); return; } + + let delay = http.formvalue('delay') || '5'; + + if (!match(`${delay}`, /^[0-9]+$/) || +delay < 2) { + error_response(400, 'Invalid delay parameter'); + return; + } + + let api_path = sprintf('%s/pods/%s/top?stream=true&delay=%s', API_BASE, name, delay); + + stream_podman(api_path, (chunk) => { + return http_write_padded(chunk); + }, false, timer); + }, + + pod_stats: (name) => { + if (validate_id(name)) { error_response(400, 'Invalid pod name or ID'); return; } + + let timer = session_timer(ctx?.authsession); // ucode-lsp disable + if (!timer) { error_response(403, 'Session expired'); return; } + + let api_path = sprintf('%s/pods/stats?namesOrIDs=%s&stream=true', API_BASE, name); + + stream_podman(api_path, (chunk) => { + return http_write_padded(chunk); + }, false, timer); + }, + + image_pull: () => { + let timer = session_timer(ctx?.authsession); // ucode-lsp disable + if (!timer) { error_response(403, 'Session expired'); return; } + + let reference = `${http.formvalue('reference')}`; + if (!reference || !match(reference, /^[a-zA-Z0-9._/:@-]+$/)) { + error_response(400, 'Invalid or missing reference parameter'); + return; + } + + if (!match(reference, /[/:@]/)) { + error_response(400, 'Reference must be a registry reference (e.g. name:tag), not a raw image ID'); + return; + } + + let sid = ctx?.authsession; // ucode-lsp disable + let ref_id = replace(reference, /[/:@]/g, '-'); + let logfile = '/tmp/podman-pull-' + ref_id + '.ndjson'; + let pidfile = '/tmp/podman-pull-' + ref_id + '.pid'; + + // Check if a worker is already running for this reference + let pid = 0; + let pf = open(pidfile, 'r'); + if (pf) { pid = +(trim(pf.read(200) || '0')); pf.close(); } + let is_running = pid > 0 && stat('/proc/' + pid); + + // Read (and reuse) the ubus connection for session offset + let uconn_rw = sid ? ubus_connect() : null; + let offset = 0; + if (uconn_rw) { + let sdat = uconn_rw.call('session', 'get', { ubus_rpc_session: sid }); + offset = +(sdat?.values?.['pull_' + ref_id] || 0); + } + + // Check if the existing logfile has bytes we haven't sent yet + // (worker may have just finished between two reconnects) + let lstat = stat(logfile); + let has_unread = lstat && offset < lstat.size; + + let is_completed_pull = false; + if (!is_running && lstat && lstat.size > 0) { + let f = open(logfile, 'r'); + if (f) { + let tail_start = lstat.size > 512 ? lstat.size - 512 : 0; + if (tail_start > 0) f.seek(tail_start); + let tail = f.read(512); + f.close(); + is_completed_pull = !!(tail && (index(tail, '"images"') >= 0 || index(tail, '"error"') >= 0)); + } + } + + // Fresh start: worker not running AND (previous pull completed, OR no unread + // progress from a prior session, OR stale offset=0 logfile from a closed tab). + let is_fresh_start = !is_running && (is_completed_pull || !(has_unread && offset > 0)); + + if (is_fresh_start) { + // Clean up any stale files and reset offset + unlink(logfile); + unlink(pidfile); + offset = 0; + if (uconn_rw) { + let vals = {}; + vals['pull_' + ref_id] = 0; + uconn_rw.call('session', 'set', { ubus_rpc_session: sid, values: vals }); + } + // Safety: single quotes are used as shell delimiters below. + // The regex on line 295 already rejects them; this is a belt-and-suspenders guard. + if (index(reference, "'") >= 0 || index(logfile, "'") >= 0 || index(pidfile, "'") >= 0) { + error_response(400, 'Invalid characters in reference'); + return; + } + system(sprintf( + "timeout 600 /usr/bin/ucode /usr/share/podman/pull-worker.uc '%s' '%s' '%s' /dev/null 2>/dev/null &", + reference, logfile, pidfile + )); + } + + http.status(200, 'OK'); + http.header('Content-Type', 'text/plain'); + http.write_headers(); + http.write('\n'); + + let t = _uci_timeouts; + let ka_ms = (t.network - 5) * 1000; + let last_saved = offset; + + uloop.init(); + + let poll_fn; + poll_fn = () => { + // Read and forward any new bytes written to the logfile + let lf = open(logfile, 'r'); + if (lf) { + if (offset > 0) lf.seek(offset); + let chunk = lf.read(8192); + lf.close(); + if (chunk && length(chunk)) { + if (!http_write_padded(chunk)) { uloop.end(); return; } + offset += length(chunk); + // Persist offset to session (rate-limited) + if (uconn_rw && offset - last_saved >= 1024) { + let vals = {}; + vals['pull_' + ref_id] = offset; + uconn_rw.call('session', 'set', { ubus_rpc_session: sid, values: vals }); + last_saved = offset; + } + } + } + + // If worker has finished and we've consumed all bytes: close cleanly + let cpid = 0; + let cpf = open(pidfile, 'r'); + if (cpf) { cpid = +(trim(cpf.read(200) || '0')); cpf.close(); } + if (!cpid || !stat('/proc/' + cpid)) { + let sf = stat(logfile); + if (!sf || offset >= sf.size) { + unlink(logfile); + uloop.end(); + return; + } + } + + uloop.timer(200, poll_fn); + }; + uloop.timer(0, poll_fn); + + // Self-rearming keepalive - prevents network_timeout during slow layer downloads + let ka; + ka = () => { http_write_padded('\n'); uloop.timer(ka_ms, ka); }; + uloop.timer(ka_ms, ka); + + // Session expiry timer - same pattern as other streams + uloop.timer(timer.timeout_ms, timer.on_expire); + + uloop.run(); + + // Persist final offset before this script exits (next reconnect will resume here) + if (uconn_rw && offset !== last_saved) { + let vals = {}; + vals['pull_' + ref_id] = offset; + uconn_rw.call('session', 'set', { ubus_rpc_session: sid, values: vals }); + } + }, + +}; diff --git a/luci-app-podman/ucode/podman_http.uc b/luci-app-podman/ucode/podman_http.uc new file mode 100644 index 000000000..a417f5146 --- /dev/null +++ b/luci-app-podman/ucode/podman_http.uc @@ -0,0 +1,76 @@ +// Copyright 2026 Christopher Söllinger +// Licensed to the public under the Apache License 2.0. +// +// Small helpers for the wire-format HTTP/1.0 conversation the rpcd plugin, +// the LuCI streaming controller and the pull-worker all have with the +// Podman API socket. None of them need a full HTTP client — just request +// construction and a couple of header parsers. + +'use strict'; + +/** + * @param {string} method GET / POST / PUT / DELETE + * @param {string} path Request-URI including any query string + * @param {string|null} body JSON body (or null/empty for GET-style requests) + * @returns {string} The complete request bytes + */ +export function build_request(method, path, body) { + let req = sprintf('%s %s HTTP/1.0\r\nHost: localhost\r\n', method, path); + if (body != null && body !== '') { + let s = `${body}`; + req += sprintf('Content-Type: application/json\r\nContent-Length: %d\r\n', length(s)); + req += '\r\n'; + req += s; + return req; + } + return req + '\r\n'; +}; + +/** + * Extract HTTP status code from the start of a response buffer. + * + * @param {string} buf Buffer that begins with the status line + * @returns {number} Status code, or 0 if the line doesn't match + */ +export function parse_status(buf) { + let m = match(buf, /^HTTP\/[0-9.]+ ([0-9]+)/); + return m ? +m[1] : 0; +}; + +/** + * Extract a Content-Length value from a response header buffer. + * + * @param {string} buf Header buffer (status line + headers) + * @returns {number} Bytes declared by Content-Length, or -1 if absent + */ +export function parse_content_length(buf) { + let m = match(buf, /[Cc]ontent-[Ll]ength:\s*([0-9]+)/); + return m ? +m[1] : -1; +}; + +/** + * Blocking read of HTTP response headers from a connected socket. Reads + * until the \r\n\r\n separator is seen, then returns the header block + * (without trailing CRLFCRLF) and any body bytes that arrived together + * with the headers. Returns null if the peer closes before headers are + * complete. + * + * @param {any} sock Connected socket + * @param {number|null} blocksize recv chunk size (default 65536) + * @returns {{header_buf:string, body_remainder:string}|null} + */ +export function read_headers(sock, blocksize) { + let bs = blocksize || 65536; + let buf = ''; + while (true) { + let chunk = sock.recv(bs); + if (!chunk) return null; + buf += `${chunk}`; + let sep = index(buf, '\r\n\r\n'); + if (type(sep) !== 'int' || sep < 0) continue; + return { + header_buf: substr(buf, 0, sep), + body_remainder: substr(buf, sep + 4) + }; + } +}; diff --git a/luci-app-podman/ucode/podman_socket.uc b/luci-app-podman/ucode/podman_socket.uc new file mode 100644 index 000000000..ad33534e2 --- /dev/null +++ b/luci-app-podman/ucode/podman_socket.uc @@ -0,0 +1,97 @@ +// Copyright 2026 Christopher Söllinger +// Licensed to the public under the Apache License 2.0. +// +// Shared Podman socket helper for the rpcd plugin, the LuCI streaming +// controller, the pull-worker and the podman-api CLI helper. Reads the +// destination once at module load from UCI: +// luci-podman.globals.socket_path +// Accepted URI schemes: +// unix:///path/to/socket AF_UNIX +// tcp://host:port AF_INET +// tcp6://[::1]:port AF_INET6 +// /path/to/socket bare path, treated as unix:///path +// Fallback: unix:///run/podman/podman.sock. + +'use strict'; + +import { cursor } from 'uci'; +import * as socket from 'socket'; + +const DEFAULT_DEST = 'unix:///run/podman/podman.sock'; + +/** Path prefix for all libpod REST API endpoints. */ +export const API_BASE = '/v5.0.0/libpod'; + +const _dest = (() => { + let c = cursor(); + let v = c.get('luci-podman', 'globals', 'socket_path'); + c.unload('luci-podman'); + return v || DEFAULT_DEST; +})(); + +const _parsed = (() => { + if (substr(_dest, 0, 1) === '/') + return { scheme: 'unix', addr: _dest }; + let m = match(_dest, /^([a-z0-9]+):\/\/(.+)$/i); + if (!m) + return null; + return { scheme: lc(m[1]), addr: m[2] }; +})(); + +/** @returns {?Socket} connected socket, or null on failure */ +export function connect() { + if (!_parsed) + return null; + + if (_parsed.scheme === 'unix') { + let sock = socket.create(socket.AF_UNIX, socket.SOCK_STREAM); + if (!sock) return null; + if (!sock.connect(socket.sockaddr(_parsed.addr))) { + sock.close(); + return null; + } + return sock; + } + + if (_parsed.scheme === 'tcp' || _parsed.scheme === 'tcp6') { + let family = _parsed.scheme === 'tcp6' ? socket.AF_INET6 : socket.AF_INET; + let raw = _parsed.addr; + let colon = rindex(raw, ':'); + if (colon < 0) return null; + let host = substr(raw, 0, colon); + let port = +substr(raw, colon + 1); + host = replace(host, /^\[|\]$/g, ''); + if (!port || !host) return null; + + let ai = socket.addrinfo(host, `${port}`, { family, socktype: socket.SOCK_STREAM }); + if (!ai || !length(ai)) return null; + let sock = socket.create(family, socket.SOCK_STREAM); + if (!sock) return null; + if (!sock.connect(ai[0].addr)) { + sock.close(); + return null; + } + return sock; + } + + return null; +}; + +/** @returns {boolean} true if destination is a TCP scheme */ +export function is_remote() { + return _parsed && (_parsed.scheme === 'tcp' || _parsed.scheme === 'tcp6'); +}; + +/** @returns {string} the raw URI from UCI (or the default), for logging */ +export function get_dest() { + return _dest; +}; + +/** + * @returns {?string} filesystem path of the unix socket, or null for tcp/tcp6. + * Useful for pre-flight stat() checks before connect(). + */ +export function get_local_path() { + if (!_parsed || _parsed.scheme !== 'unix') return null; + return _parsed.addr; +}; diff --git a/luci-app-podman/ucode/podman_validate.uc b/luci-app-podman/ucode/podman_validate.uc new file mode 100644 index 000000000..c4cb2a3ea --- /dev/null +++ b/luci-app-podman/ucode/podman_validate.uc @@ -0,0 +1,79 @@ +// Copyright 2026 Christopher Söllinger +// Licensed to the public under the Apache License 2.0. +// +// Shared parameter validators for the rpcd plugin, the LuCI streaming +// controller and the podman-api CLI helper. +// +// Convention: every exported validator returns an error message **string** +// on failure and `null` on success, so call sites can chain with || : +// +// let err = require_param('name', v) || validate_name(v); +// if (err) return { error: err }; + +'use strict'; + +const VALID_RESTART_POLICIES = { 'no': true, 'always': true, 'on-failure': true, 'unless-stopped': true }; + +const CONTAINER_BODY_KEYS = { + cpu: true, memory: true, blockIO: true, devices: true, + hugepageLimits: true, network: true, pids: true, rdma: true, unified: true, + BlkIOWeightDevice: true, + DeviceReadBPs: true, DeviceReadIOPs: true, + DeviceWriteBPs: true, DeviceWriteIOPs: true, + Env: true, UnsetEnv: true, + health_cmd: true, health_interval: true, health_log_destination: true, + health_max_log_count: true, health_max_log_size: true, + health_on_failure: true, health_retries: true, health_start_period: true, + health_startup_cmd: true, health_startup_interval: true, + health_startup_retries: true, health_startup_success: true, + health_startup_timeout: true, health_timeout: true, + no_healthcheck: true +}; + +/** @param {string} id */ +export function validate_id(id) { + if (!id || type(id) !== 'string' || !match(id, /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/)) + return 'Invalid id format'; +}; + +/** @param {string} name */ +export function validate_name(name) { + if (!name || type(name) !== 'string' || !match(name, /^[a-zA-Z0-9_.-]+$/)) + return 'Invalid name format'; +}; + +/** @param {string} ref */ +export function validate_image_ref(ref) { + if (!ref || type(ref) !== 'string' || !match(ref, /^[a-zA-Z0-9_.:\/@-]+$/)) + return 'Invalid image reference'; +}; + +/** @param {string} query */ +export function validate_query_params(query) { + if (!query || type(query) !== 'string' || !match(query, /^[a-zA-Z0-9=&_.,-]+$/)) + return 'Invalid query parameters'; +}; + +/** @param {string} policy */ +export function validate_restart_policy(policy) { + if (policy && !(policy in VALID_RESTART_POLICIES)) + return 'Invalid restart policy'; +}; + +/** @param {string} val */ +export function validate_int(val) { + if (!val || type(val) !== 'string' || !match(val, /^[0-9]+$/)) + return 'Invalid number'; +}; + +/** + * @param {string} name + * @param {any} value + */ +export function require_param(name, value) { + if (value == null || value === '' + || (type(value) === 'object' && length(keys(value)) === 0)) + return `Missing required parameter: ${name}`; +}; + +export const BODY_KEYS = CONTAINER_BODY_KEYS; diff --git a/luci-app-quickactions/Makefile b/luci-app-quickactions/Makefile new file mode 100644 index 000000000..3ee147e83 --- /dev/null +++ b/luci-app-quickactions/Makefile @@ -0,0 +1,25 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-quickactions +PKG_VERSION:=3.0.0 +PKG_RELEASE:=1 + +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=arafatrahmanzami + +LUCI_TITLE:=Quick Actions Dashboard +LUCI_DESCRIPTION:=Tabbed dashboard for OpenWrt with 14 modules: Dashboard, Essential, Tools, Logs, Services, Hotplug, Crontab, Guest WiFi, Terminal, Task Plan, Network Setup, Command, Dependencies. +LUCI_DEPENDS:=+luci-base +luci-app-commands +rpcd-mod-file +rpcd-mod-luci +ttyd +LUCI_PKGARCH:=all + +include $(TOPDIR)/feeds/luci/luci.mk + +define Package/luci-app-quickactions/conffiles +/etc/config/guestwifi +/etc/config/hotplug +/etc/config/quickactions +/etc/config/taskplan +/etc/config/wizard +endef + +$(eval $(call BuildPackage,luci-app-quickactions)) diff --git a/luci-app-quickactions/htdocs/luci-static/resources/view/system/quickactions.js b/luci-app-quickactions/htdocs/luci-static/resources/view/system/quickactions.js new file mode 100644 index 000000000..62d119289 --- /dev/null +++ b/luci-app-quickactions/htdocs/luci-static/resources/view/system/quickactions.js @@ -0,0 +1,6646 @@ +/** + * luci-app-quickactions + * All-in-one dashboard and system-control surface for OpenWrt / ImmortalWrt. + * + * Bundles 14 tabs: Dashboard, Essential, Tools, Logs, Services, Hotplug, + * Crontab, Guest WiFi, Terminal (ttyd), Task Plan, Network Setup, Command, + * Dependencies, Configuration. + * + * Licensed to the GNU General Public License v3.0. + */ + +'use strict'; +'require view'; +'require ui'; +'require uci'; +'require rpc'; +'require form'; +'require fs'; +'require poll'; +'require dom'; +'require uqr'; +'require tools.widgets as widgets'; + +// ===================================================================== +// LuCI 26.203 workaround — dom.data() crashes on stale data-idref nodes +// +// dom.content() deletes registry entries for every descendant with a +// data-idref attribute, but leaves the attribute on the node. Any later +// dom.data(node, key, value) on those nodes throws +// "Cannot set properties of undefined (setting '_class')" +// which crashes form.js's CBIMap.renderContents(), the last step of +// form.Map.save(). The writes reach the server first, but the promise +// chain rejects and the UI reports failure. +// +// Reinitialize the registry slot when we detect it's been dropped. +// ===================================================================== +(function patchDOMData() { + try { + if (!dom || dom._qaDataPatched) return; + var _orig = dom.data; + dom.data = function(node, key, val) { + if (node && typeof node.getAttribute === 'function') { + var id = node.getAttribute('data-idref'); + if (id != null && this.registry && !this.registry[id]) { + // Fires for BOTH the getter and setter paths. dom.content() + // deletes registry entries when nodes are detached but + // leaves the data-idref attribute on the node. The next + // dom.data(node, '_class') then hits + // this.registry[id][key] + // with registry[id] === undefined and throws + // "Cannot read properties of undefined (reading '_class')". + // Seed a fresh empty slot so the getter returns undefined. + this.registry[id] = {}; + } + } + return _orig.apply(this, arguments); + }; + dom._qaDataPatched = true; + console.log('[qa-fix] dom.data patched (getter+setter, data-idref)'); + } catch(e) { + console.warn('[qa-fix] failed to patch dom.data:', e); + } +})(); + + + +function getCurrentLanIp() { + var lanIpRaw = uci.get('network', 'lan', 'ipaddr'); + var lanIp = Array.isArray(lanIpRaw) ? (lanIpRaw[0] || '192.168.10.1') : (lanIpRaw || '192.168.10.1'); + return String(lanIp).split(' ')[0]; +} + +var GuestWifiViewClass = (function() { + +/* + Copyright 2026 Rafał Wabik - IceG - From eko.one.pl forum + + Licensed to the GNU General Public License v3.0. +*/ + +let DEFAULTS = { + ssid: 'Guest-WiFi', + encryption: 'psk2', + password: '', + radio: 'radio0', + isolate: '1', + macaddr: '', + ip: '172.16.0.1', + netmask: '255.240.0.0', + dhcpStart: '100', + dhcpLimit: '150', + dhcpLease: '12h', + fwForwardDest: 'wan', + fwInput: 'REJECT', + fwOutput: 'ACCEPT', + fwForward: 'REJECT', + fwDhcpPorts: '67-68', + fwDnsPort: '53' +}; + +let OWNER_TAG = 'guest_owner'; + +function addGuestWifiStyles() { + if (document.getElementById('guest-wifi-styles')) return; + let style = document.createElement('style'); + style.id = 'guest-wifi-styles'; + style.type = 'text/css'; + style.textContent = '\ + :root {\ + --gw-badge-on-bg: #34c759;\ + --gw-badge-off-bg: #7f8c8d;\ + --gw-badge-text: #ffffff;\ + --gw-badge-shadow: 0 1px 2px rgba(0,0,0,.4), 0 2px 6px rgba(0,0,0,.25);\ + --gw-badge-border-on: transparent;\ + --gw-badge-border-off: transparent;\ + }\ + :root[data-darkmode="true"] {\ + --gw-badge-on-bg: rgba(46,204,113,0.28);\ + --gw-badge-off-bg: rgba(255,255,255,0.12);\ + --gw-badge-text: #e5e7eb;\ + --gw-badge-shadow: 0 1px 2px rgba(0,0,0,.35), 0 2px 6px rgba(0,0,0,.22);\ + --gw-badge-border-on: rgba(46,204,113,0.5);\ + --gw-badge-border-off: rgba(255,255,255,0.3);\ + }\ + .gw-badge {\ + display:inline-block;\ + padding:4px 10px;\ + border-radius:4px;\ + color:var(--gw-badge-text);\ + font-size:13px;\ + font-weight:500;\ + white-space:nowrap;\ + text-align:center;\ + border:1px solid transparent;\ + text-shadow:var(--gw-badge-shadow);\ + }\ + .gw-badge-on { background:var(--gw-badge-on-bg); border-color:var(--gw-badge-border-on); }\ + .gw-badge-off { background:var(--gw-badge-off-bg); border-color:var(--gw-badge-border-off); }\ + '; + document.head.appendChild(style); + + // Layout fixups for the narrow iframe context. The outer page + // reserves sidebar space, so the iframe viewport is smaller than + // a full page; these rules keep modals and controls usable. + if (!document.getElementById('guest-wifi-layout-styles')) { + let ls = document.createElement('style'); + ls.id = 'guest-wifi-layout-styles'; + ls.type = 'text/css'; + ls.textContent = '\ + .modal.cbi-modal { width: min(95vw, 1200px) !important; max-width: 95vw !important; min-width: 0 !important; }\ + .modal.cbi-modal .cbi-value { display: flex !important; flex-wrap: nowrap !important; align-items: flex-start !important; }\ + .modal.cbi-modal .cbi-value-title { flex: 0 0 200px !important; }\ + .modal.cbi-modal .cbi-value-field { flex: 1 1 auto !important; min-width: 0 !important; }\ + .cbi-value-field { min-width: 0 !important; }\ + .cbi-value-field select,\ + .cbi-value-field input,\ + .cbi-value-field .cbi-dropdown { max-width: 100% !important; }\ + .btn.qr-code { display: inline-flex; align-items: center; gap: .5em; }\ + .btn.qr-code svg { display: block; height: 1.5em; width: auto; }\ + html, body { background-color: var(--background-color-default, #141414) !important; background-image: none !important; }\ + #view.spinning::before,\ + #view.spinning::after,\ + body > .spinning,\ + body > .loading-overlay,\ + body > .spinner { display: none !important; visibility: hidden !important; }\ + .cbi-input-password { padding-right: 34px !important; }\ + '; + document.head.appendChild(ls); + } +} + +// Un-cap .container inside the embed page. The embed view is a full +// LuCI page loaded at ?embed=guestwifi, so it has its own theme +// wrapper (.container) that caps width at ~940px. Un-cap here so the +// guest wifi form and its modals use the entire iframe viewport. +(function(){ + if (document.getElementById('qa-embed-fullwidth')) return; + var s = document.createElement('style'); + s.id = 'qa-embed-fullwidth'; + s.type = 'text/css'; + s.textContent = '.container,.main,.main-right,.cbi-map,.fs-content,.fs-main{' + + 'max-width:none !important;width:100% !important;' + + 'padding-left:0 !important;padding-right:0 !important;' + + 'margin-left:0 !important;margin-right:0 !important;}' + + 'html,body{height:auto !important;min-height:0 !important;}' + + 'body>.container,body>#view,body>main{min-height:0 !important;}' + document.head.appendChild(s); +})(); + +function ssidBadge(section_id, text) { + let val = uci.get('guestwifi', section_id, 'enable'); + let enabled = (val == null) ? true : (val === '1'); + let cls = 'gw-badge ' + (enabled ? 'gw-badge-on' : 'gw-badge-off'); + return E('span', { 'class': cls }, text || ''); +} + +function radioBadge(section_id, radioName, wifiDevices) { + let val = uci.get('guestwifi', section_id, 'enable'); + let enabled = (val == null) ? true : (val === '1'); + let label = radioLabel(radioName, wifiDevices); + + return E('span', { 'class': 'ifacebadge' }, [ + E('img', { 'src': L.resource('icons/wifi%s.svg').format(enabled ? '' : '_disabled') }), + ' ', + label, + '\u00A0' + ]); +} + +let ENCRYPTION_MODES = [ + ['psk2', 'WPA2-PSK'], + ['sae', 'WPA3-SAE'], + ['sae-mixed', 'WPA2-PSK/WPA3-SAE ' + _('Mixed Mode')], + ['psk-mixed', 'WPA-PSK/WPA2-PSK ' + _('Mixed Mode')], + ['psk', 'WPA-PSK'], + ['owe', 'OWE (' + _('Enhanced Open') + ')'], + ['wep-open', _('WEP Open System')], + ['wep-shared', _('WEP Shared Key')], + ['none', _('No encryption (open network)')] +]; + +function encryptionLabel(key) { + let hit = ENCRYPTION_MODES.filter(function(m) { return m[0] === key; })[0]; + return hit ? hit[1] : key; +} + +let PASSWORD_CHARSETS = { + upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + lower: 'abcdefghijklmnopqrstuvwxyz', + digits: '1234567890', + special: '!@#$%^&*()-_=+[]{}?' +}; + +function secureRandomInt(max) { + let cryptoObj = window.crypto || window.msCrypto; + let arr = new Uint32Array(1); + + if (cryptoObj && cryptoObj.getRandomValues) { + cryptoObj.getRandomValues(arr); + return arr[0] % max; + } + + return Math.floor(Math.random() * max); +} + +function shuffleArray(arr) { + for (let i = arr.length - 1; i > 0; i--) { + let j = secureRandomInt(i + 1); + let tmp = arr[i]; + arr[i] = arr[j]; + arr[j] = tmp; + } + return arr; +} + +function generateStrongPassword(length, components) { + let chosenSets = (components || []).map(function(c) { return PASSWORD_CHARSETS[c]; }).filter(Boolean); + + if (!chosenSets.length) + chosenSets = [PASSWORD_CHARSETS.lower, PASSWORD_CHARSETS.digits]; + + length = Math.max(parseInt(length, 10) || 0, chosenSets.length); + length = Math.min(Math.max(length, 8), 63); + + let pool = chosenSets.join(''); + let result = chosenSets.map(function(set) { return set.charAt(secureRandomInt(set.length)); }); + + for (let i = result.length; i < length; i++) + result.push(pool.charAt(secureRandomInt(pool.length))); + + return shuffleArray(result).join(''); +} + +let cbiPasswordComponentsValue = form.ListValue.extend({ + renderWidget: function(section_id, option_index, cfgvalue) { + let choices = this.transformChoices(); + let widget = new ui.Dropdown( + (cfgvalue != null) ? cfgvalue : this.default, + choices, + { + id: this.cbid(section_id), + sort: this.keylist, + optional: true, + multiple: true, + display_items: 4, + dropdown_items: 4, + select_placeholder: this.placeholder, + validate: L.bind(this.validate, this, section_id), + disabled: (this.readonly != null) ? this.readonly : this.map.readonly + } + ); + + window.__guestPasswordComponentsDropdown = window.__guestPasswordComponentsDropdown || {}; + window.__guestPasswordComponentsDropdown[section_id] = widget; + + return widget.render(); + } +}); + +function buildSVGQRCode(data, code, options, dummy) { + let opts = Object.assign({ + pixelSize: 4, + whiteColor: 'white', + blackColor: 'black', + ecc: 'M' + }, options); + + let svg = uqr.renderSVG(data, opts); + + if (dummy) + return svg; + + code.style.opacity = ''; + dom.content(code, Object.assign(E(svg), { style: 'width:100%;height:auto' })); +} + +function assignNetIndexes() { + let defs = uci.sections('guestwifi', 'guest'); + let used = {}; + + defs.forEach(function(def) { + let idx = parseInt(def.net_idx, 10); + if (idx > 0) + used[idx] = true; + }); + + let nextIdx = 1; + function takeNextFreeIdx() { + while (used[nextIdx]) nextIdx++; + used[nextIdx] = true; + return nextIdx; + } + + defs.forEach(function(def) { + let idx = parseInt(def.net_idx, 10); + if (!(idx > 0)) + uci.set('guestwifi', def['.name'], 'net_idx', String(takeNextFreeIdx())); + }); +} + +function namesForNetIndex(idx) { + let suf = (idx > 1) ? String(idx) : ''; + return { + networkName: 'guest' + (suf ? '_' + suf : ''), + deviceName: 'br-guest' + suf, + wifiName: 'guestwifi' + (suf ? '_' + suf : ''), + dhcpName: 'guest' + (suf ? '_' + suf : '') + }; +} + +let IP_POOL_FIRST_OCTET = 172; +let IP_POOL_SECOND_FIRST = 16; +let IP_POOL_SECOND_LAST = 31; + +function usedInterfaceIps(excludeSid) { + let used = {}; + + uci.sections('guestwifi', 'guest').forEach(function(s) { + if (s['.name'] === excludeSid) return; + if (s.interface_ip) used[s.interface_ip] = true; + }); + uci.sections('network', 'interface').forEach(function(s) { + if (excludeSid != null && s[OWNER_TAG] === excludeSid) return; + if (s.ipaddr) used[s.ipaddr] = true; + }); + + return used; +} + +function randomFreeGuestIp(excludeSid) { + let used = usedInterfaceIps(excludeSid); + let candidates = []; + + for (let o2 = IP_POOL_SECOND_FIRST; o2 <= IP_POOL_SECOND_LAST; o2++) { + let ip = IP_POOL_FIRST_OCTET + '.' + o2 + '.0.1'; + if (!used[ip]) + candidates.push(ip); + } + + if (!candidates.length) + return null; + + return candidates[Math.floor(Math.random() * candidates.length)]; +} + +function bandLabelForDevice(dev) { + if (!dev) + return ''; + + let band = dev.band; + if (band === '2g') return '2.4 GHz'; + if (band === '5g') return '5 GHz'; + if (band === '6g') return '6 GHz'; + + let hw = dev.hwmode || ''; + if (/^11a/.test(hw) && !/^11ax/.test(hw)) { + return '5 GHz'; + } + if (/^11(b|g)/.test(hw)) + return '2.4 GHz'; + + let htmode = dev.htmode || ''; + if (/^HE160|HE80|VHT/.test(htmode)) + return '5 GHz'; + + return ''; +} + +function radioLabel(radioName, wifiDevices) { + let dev = wifiDevices.filter(function(d) { return d['.name'] === radioName; })[0]; + let band = bandLabelForDevice(dev); + return band ? (radioName + ' (' + band + ')') : radioName; +} + +function cleanupGuestConfig(validSids, ownerSid) { + function shouldRemove(s) { + let owner = s[OWNER_TAG]; + if (!owner) + return false; + if (ownerSid != null) + return owner === ownerSid; + return validSids.indexOf(owner) === -1; + } + + uci.sections('network', 'interface').forEach(function(s) { + if (shouldRemove(s)) uci.remove('network', s['.name']); + }); + uci.sections('network', 'device').forEach(function(s) { + if (shouldRemove(s)) uci.remove('network', s['.name']); + }); + uci.sections('wireless', 'wifi-iface').forEach(function(s) { + if (shouldRemove(s)) uci.remove('wireless', s['.name']); + }); + uci.sections('dhcp', 'dhcp').forEach(function(s) { + if (shouldRemove(s)) uci.remove('dhcp', s['.name']); + }); + uci.sections('firewall', 'zone').forEach(function(s) { + if (shouldRemove(s)) uci.remove('firewall', s['.name']); + }); + uci.sections('firewall', 'forwarding').forEach(function(s) { + if (shouldRemove(s)) uci.remove('firewall', s['.name']); + }); + uci.sections('firewall', 'rule').forEach(function(s) { + if (shouldRemove(s)) uci.remove('firewall', s['.name']); + }); +} + +function collectOwnerSids() { + let sids = {}; + + uci.sections('network', 'interface').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + uci.sections('network', 'device').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + uci.sections('wireless', 'wifi-iface').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + uci.sections('dhcp', 'dhcp').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + uci.sections('firewall', 'zone').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + uci.sections('firewall', 'forwarding').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + uci.sections('firewall', 'rule').forEach(function(s) { if (s[OWNER_TAG]) sids[s[OWNER_TAG]] = true; }); + + return Object.keys(sids); +} + +function importGuestNetwork(sid) { + let iface = uci.sections('network', 'interface').filter(function(s) { return s[OWNER_TAG] === sid; })[0]; + let dhcp = uci.sections('dhcp', 'dhcp').filter(function(s) { return s[OWNER_TAG] === sid; })[0]; + let wifi = uci.sections('wireless', 'wifi-iface').filter(function(s) { return s[OWNER_TAG] === sid; })[0]; + let zone = uci.sections('firewall', 'zone').filter(function(s) { return s[OWNER_TAG] === sid; })[0]; + let fwd = uci.sections('firewall', 'forwarding').filter(function(s) { return s[OWNER_TAG] === sid; })[0]; + let rules = uci.sections('firewall', 'rule').filter(function(s) { return s[OWNER_TAG] === sid; }); + + let dhcpRule = rules.filter(function(r) { return r.proto === 'udp'; })[0]; + let dnsRule = rules.filter(function(r) { return r.proto === 'tcpudp'; })[0]; + + uci.add('guestwifi', 'guest', sid); + uci.set('guestwifi', sid, 'enable', (wifi && wifi.disabled === '1') ? '0' : '1'); + + if (wifi) { + uci.set('guestwifi', sid, 'ssid', wifi.ssid || DEFAULTS.ssid); + uci.set('guestwifi', sid, 'radio', wifi.device || DEFAULTS.radio); + uci.set('guestwifi', sid, 'isolate', wifi.isolate || DEFAULTS.isolate); + if (wifi.macaddr) + uci.set('guestwifi', sid, 'macaddr', wifi.macaddr); + + if (!wifi.encryption || wifi.encryption === 'none' || wifi.encryption === 'owe') { + uci.set('guestwifi', sid, 'encryption', wifi.encryption || 'none'); + } else if (wifi.encryption === 'wep-open' || wifi.encryption === 'wep-shared') { + uci.set('guestwifi', sid, 'encryption', wifi.encryption); + uci.set('guestwifi', sid, 'password', wifi.key1 || DEFAULTS.password); + } else { + uci.set('guestwifi', sid, 'encryption', wifi.encryption); + uci.set('guestwifi', sid, 'password', wifi.key || DEFAULTS.password); + } + } + + if (iface) { + uci.set('guestwifi', sid, 'interface_ip', iface.ipaddr || DEFAULTS.ip); + uci.set('guestwifi', sid, 'netmask', iface.netmask || DEFAULTS.netmask); + } + + if (dhcp) { + uci.set('guestwifi', sid, 'dhcp_start', dhcp.start || DEFAULTS.dhcpStart); + uci.set('guestwifi', sid, 'dhcp_limit', dhcp.limit || DEFAULTS.dhcpLimit); + uci.set('guestwifi', sid, 'dhcp_lease', dhcp.leasetime || DEFAULTS.dhcpLease); + } + + if (zone) { + uci.set('guestwifi', sid, 'fw_input', zone.input || DEFAULTS.fwInput); + uci.set('guestwifi', sid, 'fw_output', zone.output || DEFAULTS.fwOutput); + uci.set('guestwifi', sid, 'fw_forward', zone.forward || DEFAULTS.fwForward); + } + + if (fwd) { + uci.set('guestwifi', sid, 'fw_forward_dest', fwd.dest || DEFAULTS.fwForwardDest); + } + + if (dhcpRule) { + uci.set('guestwifi', sid, 'fw_dhcp_ports', dhcpRule.dest_port || DEFAULTS.fwDhcpPorts); + } + + if (dnsRule) { + uci.set('guestwifi', sid, 'fw_dns_port', dnsRule.dest_port || DEFAULTS.fwDnsPort); + } +} + +function knownGuestSids() { + return uci.sections('guestwifi', 'guest').map(function(s) { return s['.name']; }); +} + +function resolveDuplicateInterfaceIps() { + let seen = {}; + + uci.sections('guestwifi', 'guest').forEach(function(def) { + let sid = def['.name']; + let ip = def.interface_ip; + if (!ip) return; + + if (seen[ip]) { + let newIp = randomFreeGuestIp(sid); + if (newIp) { + uci.set('guestwifi', sid, 'interface_ip', newIp); + seen[newIp] = true; + } + } else { + seen[ip] = true; + } + }); +} + +function guessLegacyInterfaceNames() { + return uci.sections('network', 'interface').filter(function(s) { + if (s[OWNER_TAG]) return false; + if (/^cfg[0-9a-f]+$/i.test(s['.name'])) return false; + return /guest/i.test(s['.name']) || /guest/i.test(s.device || ''); + }).map(function(s) { return s['.name']; }); +} + +function zoneMatchesNetwork(zone, ifname) { + let net = zone.network; + if (Array.isArray(net)) return net.indexOf(ifname) !== -1; + return net === ifname; +} + +function importLegacyGuestNetwork(ifname) { + let sid = ifname; + + uci.set('network', ifname, OWNER_TAG, sid); + + let iface = uci.sections('network', 'interface').filter(function(s) { return s['.name'] === ifname; })[0]; + let deviceName = iface ? iface.device : null; + + if (deviceName) { + let dev = uci.sections('network', 'device').filter(function(d) { return d.name === deviceName; })[0]; + if (dev) uci.set('network', dev['.name'], OWNER_TAG, sid); + } + + let dhcp = uci.sections('dhcp', 'dhcp').filter(function(d) { return d.interface === ifname; })[0]; + if (dhcp) uci.set('dhcp', dhcp['.name'], OWNER_TAG, sid); + + let wifi = uci.sections('wireless', 'wifi-iface').filter(function(w) { return w.network === ifname; })[0]; + if (wifi) uci.set('wireless', wifi['.name'], OWNER_TAG, sid); + + let zone = uci.sections('firewall', 'zone').filter(function(z) { return zoneMatchesNetwork(z, ifname); })[0]; + if (zone) { + uci.set('firewall', zone['.name'], OWNER_TAG, sid); + + let zoneName = zone.name || zone['.name']; + uci.sections('firewall', 'forwarding').forEach(function(f) { + if (f.src === zoneName) uci.set('firewall', f['.name'], OWNER_TAG, sid); + }); + uci.sections('firewall', 'rule').forEach(function(r) { + if (r.src === zoneName) uci.set('firewall', r['.name'], OWNER_TAG, sid); + }); + } + + importGuestNetwork(sid); +} + +function importExistingGuestNetworks() { + collectOwnerSids().forEach(function(sid) { + if (knownGuestSids().indexOf(sid) === -1) + importGuestNetwork(sid); + }); + + guessLegacyInterfaceNames().forEach(function(ifname) { + if (knownGuestSids().indexOf(ifname) === -1) + importLegacyGuestNetwork(ifname); + }); +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('network'), + uci.load('wireless'), + uci.load('dhcp'), + uci.load('firewall'), + uci.load('guestwifi') + ]).then(function() { + importExistingGuestNetworks(); + }); + }, + + buildGuestNetwork: function(sid) { + let get = function(name, def) { return uci.get('guestwifi', sid, name) || def; }; + + let enabled = get('enable', '1') === '1'; + // Persist enable explicitly — form.Flag.default is not written to + // UCI on its own, so without this the toggle renders off even when + // the app treats a missing enable key as enabled. + uci.set('guestwifi', sid, 'enable', enabled ? '1' : '0'); + let ssid = get('ssid', DEFAULTS.ssid); + let encryption = get('encryption', DEFAULTS.encryption); + let password = get('password', DEFAULTS.password); + let radio = get('radio', DEFAULTS.radio); + let isolate = get('isolate', DEFAULTS.isolate); + let macaddr = get('macaddr', DEFAULTS.macaddr); + let ip = get('interface_ip', DEFAULTS.ip); + let netmask = get('netmask', DEFAULTS.netmask); + let dhcpStart = get('dhcp_start', DEFAULTS.dhcpStart); + let dhcpLimit = get('dhcp_limit', DEFAULTS.dhcpLimit); + let dhcpLease = get('dhcp_lease', DEFAULTS.dhcpLease); + let fwDest = get('fw_forward_dest', DEFAULTS.fwForwardDest); + let fwInput = get('fw_input', DEFAULTS.fwInput); + let fwOutput = get('fw_output', DEFAULTS.fwOutput); + let fwForward = get('fw_forward', DEFAULTS.fwForward); + let fwDhcpPorts= get('fw_dhcp_ports', DEFAULTS.fwDhcpPorts); + let fwDnsPort = get('fw_dns_port', DEFAULTS.fwDnsPort); + + let netIdx = parseInt(get('net_idx', ''), 10); + let names = namesForNetIndex(netIdx > 0 ? netIdx : 1); + let networkName = names.networkName; + let deviceName = names.deviceName; + let wifiName = names.wifiName; + let dhcpName = names.dhcpName; + + // Original 4IceG/luci-app-guest-wifi-js form — uci.add() unconditional + uci.add('network', 'interface', networkName); + uci.set('network', networkName, OWNER_TAG, sid); + uci.set('network', networkName, 'device', deviceName); + uci.set('network', networkName, 'proto', 'static'); + uci.set('network', networkName, 'ipaddr', ip); + uci.set('network', networkName, 'netmask', netmask); + + let devSid = uci.add('network', 'device'); + uci.set('network', devSid, OWNER_TAG, sid); + uci.set('network', devSid, 'name', deviceName); + uci.set('network', devSid, 'type', 'bridge'); + uci.set('network', devSid, 'bridge_empty', '1'); + + uci.add('dhcp', 'dhcp', dhcpName); + uci.set('dhcp', dhcpName, OWNER_TAG, sid); + uci.set('dhcp', dhcpName, 'start', dhcpStart); + uci.set('dhcp', dhcpName, 'limit', dhcpLimit); + uci.set('dhcp', dhcpName, 'leasetime', dhcpLease); + uci.set('dhcp', dhcpName, 'interface', networkName); + uci.set('dhcp', dhcpName, 'dhcpv4', 'server'); + uci.set('dhcp', dhcpName, 'ignore', enabled ? '0' : '1'); + + uci.add('wireless', 'wifi-iface', wifiName); + uci.set('wireless', wifiName, OWNER_TAG, sid); + uci.set('wireless', wifiName, 'device', radio); + uci.set('wireless', wifiName, 'mode', 'ap'); + uci.set('wireless', wifiName, 'network', networkName); + uci.set('wireless', wifiName, 'ssid', ssid); + uci.set('wireless', wifiName, 'isolate', isolate); + if (macaddr) + uci.set('wireless', wifiName, 'macaddr', macaddr); + uci.set('wireless', wifiName, 'disabled', enabled ? '0' : '1'); + + if (encryption === 'none' || encryption === 'owe') { + uci.set('wireless', wifiName, 'encryption', encryption); + } else if (encryption === 'wep-open' || encryption === 'wep-shared') { + uci.set('wireless', wifiName, 'encryption', encryption); + uci.set('wireless', wifiName, 'key', '1'); + uci.set('wireless', wifiName, 'key1', password); + } else { + uci.set('wireless', wifiName, 'encryption', encryption); + uci.set('wireless', wifiName, 'key', password); + } + + let zoneSid = uci.add('firewall', 'zone'); + uci.set('firewall', zoneSid, OWNER_TAG, sid); + uci.set('firewall', zoneSid, 'name', networkName); + uci.set('firewall', zoneSid, 'network', [ networkName ]); + uci.set('firewall', zoneSid, 'input', fwInput); + uci.set('firewall', zoneSid, 'output', fwOutput); + uci.set('firewall', zoneSid, 'forward', fwForward); + + let fwdSid = uci.add('firewall', 'forwarding'); + uci.set('firewall', fwdSid, OWNER_TAG, sid); + uci.set('firewall', fwdSid, 'src', networkName); + uci.set('firewall', fwdSid, 'dest', fwDest); + + let dhcpRuleSid = uci.add('firewall', 'rule'); + uci.set('firewall', dhcpRuleSid, OWNER_TAG, sid); + uci.set('firewall', dhcpRuleSid, 'src', networkName); + uci.set('firewall', dhcpRuleSid, 'proto', 'udp'); + uci.set('firewall', dhcpRuleSid, 'src_port', fwDhcpPorts); + uci.set('firewall', dhcpRuleSid, 'dest_port', fwDhcpPorts); + uci.set('firewall', dhcpRuleSid, 'target', 'ACCEPT'); + uci.set('firewall', dhcpRuleSid, 'family', 'ipv4'); + + let dnsRuleSid = uci.add('firewall', 'rule'); + uci.set('firewall', dnsRuleSid, OWNER_TAG, sid); + uci.set('firewall', dnsRuleSid, 'src', networkName); + uci.set('firewall', dnsRuleSid, 'dest_port', fwDnsPort); + uci.set('firewall', dnsRuleSid, 'target', 'ACCEPT'); + uci.set('firewall', dnsRuleSid, 'family', 'ipv4'); + uci.set('firewall', dnsRuleSid, 'proto', 'tcpudp'); + }, + + applyAll: function(m) { + let self = this; + // form.Map.save() can throw synchronously (from checkDepends -> + // triggerValidation -> getUIElement) if a dependent option's DOM + // node lost its data-idref registry slot. Wrap in a resolved + // promise so the .catch() at the end of this chain actually fires. + return Promise.resolve().then(function() { + return m.save(); + }).then(function() { + resolveDuplicateInterfaceIps(); + assignNetIndexes(); + let defs = uci.sections('guestwifi', 'guest'); + let validSids = defs.map(function(s) { return s['.name']; }); + cleanupGuestConfig(validSids, null); + defs.forEach(function(def) { + let sid = def['.name']; + cleanupGuestConfig(validSids, sid); + self.buildGuestNetwork(sid); + }); + return uci.save(); + }).then(function() { + // Auto-apply removed: only Save & Apply triggers it. + }).catch(function(err) { + var msg = (err && err.message) ? err.message : String(err); + if (/No data|code 5|Permission denied|code 6/i.test(msg)) { + return; + } + ui.addNotification(null, E('p', {}, _('Error saving configuration:') + ' ' + msg), 'error'); + return Promise.reject(err); + }); + }, + + render: function() { + let self = this; + + addGuestWifiStyles(); + + // Modal sweep: LuCI pre-renders the modal once and toggles + // its display. No DOM insertion event fires on open, so we + // actively sweep for visible modals and force the width via + // inline !important. Runs on click, on interval, and once now. + if (!window._qaWidenModals) { + window._qaWidenModals = function() { + try { + var mods = document.querySelectorAll('.modal, .cbi-modal'); + for (var i = 0; i < mods.length; i++) { + var m = mods[i]; + var r = m.getBoundingClientRect(); + if (r.width < 100 || r.height < 50) continue; + m.style.setProperty('width', 'min(95vw, 2200px)', 'important'); + m.style.setProperty('max-width', 'calc(100vw - 32px)', 'important'); + m.style.setProperty('min-width', '0', 'important'); + } + } catch(e) {} + }; + if (!window._qaModalSweepTimer) { + window._qaModalSweepTimer = setInterval(window._qaWidenModals, 400); + } + document.addEventListener('click', function() { + setTimeout(window._qaWidenModals, 30); + setTimeout(window._qaWidenModals, 150); + setTimeout(window._qaWidenModals, 400); + }, true); + } + setTimeout(window._qaWidenModals, 100); + + let wifiDevices = uci.sections('wireless', 'wifi-device'); + let radios = wifiDevices.map(function(d) { return d['.name']; }); + if (!radios.length) + radios = ['radio0', 'radio1', 'radio2']; + + let m = new form.Map('guestwifi', _('Guest Wi-Fi'), + _('A user interface for easily creating and management isolated Wi-Fi networks for guests, each with its own access point, DHCP, and firewall rules.')); + + self.map = m; + + // The map's own page-actions (Save / Save & Apply buttons) + // call map.handleSave / map.handleSaveApply — form.Map's + // defaults — which only run form.Map.save(). They never reach + // applyAll(), so the runtime network/wireless/dhcp/firewall + // sections tagged with guest_owner were never created. + // Route the map's handlers through applyAll() as well. + m.handleSave = function(ev) { + return self.applyAll(m); + }; + m.handleSaveApply = function(ev, mode) { + return self.applyAll(m); + }; + + let s = m.section(form.GridSection, 'guest', _('Guest networks')); + s.anonymous = true; + s.addremove = true; + s.sortable = true; + s.nodescriptions = true; + s.addbtntitle = _('Add new guest network...'); + + let o = s.option(form.Flag, 'enable', _('Enabled'), + _('Enable guest network.')); + o.rmempty = false; + o.default = '1'; + o.editable = true; + // Read side: show ON for a fresh section, else UCI value. + o.cfgvalue = function(section_id) { + let v = uci.get('guestwifi', section_id, 'enable'); + return (v == null || v === '') ? '1' : v; + }; + // Formvalue: read the checkbox from the DOM directly. LuCI's + // base formvalue() returns null when getUIElement() can't find + // the widget, and base save() then writes '0' (this.remove()). + o.formvalue = function(section_id) { + // The element at this.cbid() is often a wrapper div, not the + // actual . Dig one level. + let cbid = this.cbid(section_id); + let el = document.getElementById(cbid); + let inp = el; + if (inp && inp.tagName !== 'INPUT') + inp = inp.querySelector('input[type="checkbox"]'); + if (inp && typeof inp.checked === 'boolean') + return inp.checked ? '1' : '0'; + // Fallback: search the whole row for the checkbox + if (el && el.closest) { + let rowCb = el.closest('tr, .cbi-rowstyle-1, .cbi-rowstyle-2, .tr'); + if (rowCb) { + let rInput = rowCb.querySelector('input[type="checkbox"]'); + if (rInput && typeof rInput.checked === 'boolean') + return rInput.checked ? '1' : '0'; + } + } + let v = uci.get('guestwifi', section_id, 'enable'); + return (v == null || v === '') ? '1' : v; + }; + // Write: bypass Flag.save()'s default-skip and uci.set direct. + o.write = function(section_id, formvalue) { + if (formvalue == null || formvalue === '') + formvalue = '1'; + let on = (formvalue === '1' || formvalue === true || + formvalue === 1 || formvalue === 'on'); + uci.set('guestwifi', section_id, 'enable', on ? '1' : '0'); + }; + + o = s.option(form.Value, 'ssid', _('Network name (SSID)')); + o.rmempty = false; + o.default = DEFAULTS.ssid; + o.textvalue = function(section_id) { + let val = this.cfgvalue(section_id) || this.default; + return ssidBadge(section_id, val); + }; + + o = s.option(form.ListValue, 'radio', _('Radio'), + _('The radio on which the guest network will work.')); + radios.forEach(function(r) { o.value(r, radioLabel(r, wifiDevices)); }); + o.default = DEFAULTS.radio; + o.rmempty = false; + o.textvalue = function(section_id) { + let val = this.cfgvalue(section_id) || this.default; + return radioBadge(section_id, val, wifiDevices); + }; + + o = s.option(form.ListValue, 'encryption', _('Encryption'), + _('WPA2-PSK is recommended for most devices. WPA3-SAE offers the strongest security but requires WPA3-capable clients.')); + ENCRYPTION_MODES.forEach(function(m) { o.value(m[0], m[1]); }); + o.default = DEFAULTS.encryption; + o.rmempty = false; + o.textvalue = function(section_id) { + let val = this.cfgvalue(section_id) || this.default; + return encryptionLabel(val); + }; + + o = s.option(form.Value, 'password', _('Password'), ' '); + o.password = true; + o.depends('encryption', 'psk'); + o.depends('encryption', 'psk2'); + o.depends('encryption', 'psk-mixed'); + o.depends('encryption', 'sae'); + o.depends('encryption', 'sae-mixed'); + o.depends('encryption', 'wep-open'); + o.depends('encryption', 'wep-shared'); + o.default = DEFAULTS.password; + o.modalonly = true; + o.validate = function(section_id, value) { + let enc = this.map.lookupOption('encryption', section_id)[0].formvalue(section_id); + + let inputEl = document.getElementById(this.cbid(section_id)); + let strength = inputEl ? inputEl.parentNode.querySelector('.cbi-value-description') : null; + let strongRegex = new RegExp("^(?=.{8,})(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*\\W).*$", "g"), + mediumRegex = new RegExp("^(?=.{7,})(((?=.*[A-Z])(?=.*[a-z]))|((?=.*[A-Z])(?=.*[0-9]))|((?=.*[a-z])(?=.*[0-9]))).*$", "g"), + enoughRegex = new RegExp("(?=.{6,}).*", "g"); + + if (strength) { + if (!value || !value.length) + strength.innerHTML = ''; + else if (false == enoughRegex.test(value)) + strength.innerHTML = '%s: %s'.format(_('Password strength'), _('More Characters')); + else if (strongRegex.test(value)) + strength.innerHTML = '%s: %s'.format(_('Password strength'), _('Strong')); + else if (mediumRegex.test(value)) + strength.innerHTML = '%s: %s'.format(_('Password strength'), _('Medium')); + else + strength.innerHTML = '%s: %s'.format(_('Password strength'), _('Weak')); + } + + if (!value) + return true; + + if (enc === 'wep-open' || enc === 'wep-shared') { + let isHex = /^[0-9a-fA-F]+$/.test(value); + let validLength = isHex ? (value.length === 10 || value.length === 26) + : (value.length === 5 || value.length === 13); + if (!validLength) + return _('WEP key must be 5 or 13 ASCII characters, or 10 or 26 hexadecimal digits.'); + return true; + } + + if (enc !== 'none' && enc !== 'owe' && value.length < 8) + return _('Password must be at least 8 characters long (WPA-PSK/WPA2-PSK/WPA3-SAE).'); + + return true; + }; + + let passwordDependsOn = function(opt) { + opt.depends('encryption', 'psk'); + opt.depends('encryption', 'psk2'); + opt.depends('encryption', 'psk-mixed'); + opt.depends('encryption', 'sae'); + opt.depends('encryption', 'sae-mixed'); + opt.depends('encryption', 'wep-open'); + opt.depends('encryption', 'wep-shared'); + }; + + o = s.option(form.Value, 'password_length', _('Password length'), + _('Minimum length is 8 characters, 13 characters recommended.')); + o.datatype = 'range(8,63)'; + o.default = '13'; + o.rmempty = true; + o.modalonly = true; + passwordDependsOn(o); + + o = s.option(cbiPasswordComponentsValue, 'password_components', _('Password composition'), + _('Character types to use when generating a password.')); + o.value('upper', _('Uppercase letters')); + o.value('lower', _('Lowercase letters')); + o.value('digits', _('Digits')); + o.value('special', _('Special characters')); + o.default = ['upper', 'lower', 'digits', 'special']; + o.placeholder = _('Select password components...'); + o.rmempty = true; + o.modalonly = true; + passwordDependsOn(o); + + o = s.option(form.Button, '_password_generate', _('Generate password')); + o.inputtitle = _('Generate'); + o.inputstyle = 'action'; + o.modalonly = true; + passwordDependsOn(o); + o.onclick = function(ev, section_id) { + let section = this.section; + let lengthEl = section.getUIElement(section_id, 'password_length'); + let componentsEl = section.getUIElement(section_id, 'password_components'); + let passwordEl = section.getUIElement(section_id, 'password'); + + let length = lengthEl ? parseInt(lengthEl.getValue(), 10) : 16; + let components = componentsEl ? componentsEl.getValue() : []; + + if (!Array.isArray(components)) + components = components ? [components] : []; + + let newPassword = generateStrongPassword(length, components); + + if (passwordEl) { + passwordEl.setValue(newPassword); + if (typeof passwordEl.triggerValidation === 'function') + passwordEl.triggerValidation(); + } + }; + + o = s.option(form.DummyValue, '_qrops', _('QR Code'), + _('Generates a QR code with the guest network data (SSID, encryption, password) so a client device can scan and connect.')); + o.modalonly = true; + + o.createWiFiPassword = function(section_id) { + function pctEncode(str) { + let bytes = new TextEncoder().encode(str); + let out = ''; + + for (let i = 0; i < bytes.length; i++) { + let b = bytes[i]; + let printable = (b >= 0x20 && b <= 0x3A && b !== 0x3B) || (b >= 0x3C && b <= 0x7E); + + if (printable) + out += String.fromCharCode(b); + else + out += '%' + b.toString(16).toUpperCase().padStart(2, '0'); + } + + return out; + } + + let wifiSSID = this.section.formvalue(section_id, 'ssid'); + let wifiEncr = this.section.formvalue(section_id, 'encryption') || ''; + let wifiKey = this.section.formvalue(section_id, 'password'); + + let trdisable = ''; + if (wifiEncr === 'sae') trdisable = 0; + else if (wifiEncr === 'owe') trdisable = 3; + + return [ + 'WIFI:', + (wifiKey) ? 'T:WPA;' : null, + (trdisable !== '') ? 'R:' + trdisable + ';' : null, + 'S:' + wifiSSID + ';', + (wifiKey) ? 'P:' + pctEncode(wifiKey) + ';' : null + ].filter(Boolean).join('') + ';'; + }; + + o.handleGenerateQR = function(section_id, ev) { + let parent = s.map; + let mapNode = document.querySelector('body.modal-overlay-active > #modal_overlay > .modal.cbi-modal > .cbi-map:not(.hidden)'); + let headNode = mapNode.parentNode.querySelector('h4'); + let wifiQRGenerator = this.createWiFiPassword.bind(this, section_id); + + return Promise.all([ + parent.save(null, true) + ]).then(function() { + let qrm, qrs, qro; + + qrm = new form.JSONMap({ qrcode: {} }, null, _('Scan this QR code with the client device.')); + qrm.parent = parent; + + qrs = qrm.section(form.NamedSection, 'qrcode'); + + function handleQRParamChange(ev, section_id, value) { + let code = this.map.findElement('.qr-code'); + let conf = this.map.findElement('.wifi-qr-code-content'); + let ecc = this.section.getUIElement(section_id, 'ecc'); + + if (this.isValid(section_id)) { + conf.firstChild.data = wifiQRGenerator(section_id); + code.style.opacity = '.5'; + buildSVGQRCode(conf.firstChild.data, code, { ecc: ecc.getValue() }); + } + } + + qro = qrs.option(form.ListValue, 'ecc', _('QR Error Correction Code Level')); + qro.value('L', _('Low')); + qro.value('M', _('Medium')); + qro.value('Q', _('Quartile')); + qro.value('H', _('High')); + qro.onchange = handleQRParamChange; + + qro = qrs.option(form.DummyValue, 'output'); + qro.renderWidget = function() { + let wifi_qr = wifiQRGenerator(section_id); + let ecc = this.section.formvalue(section_id, 'ecc'); + + return E('div', { + 'class': 'qr-code-display', + 'style': 'display:flex; flex-wrap:wrap; align-items:center; gap:.5em' + }, [ + E('div', { 'class': 'qr-code' }, [ + E(buildSVGQRCode(wifi_qr, null, { ecc: ecc || undefined }, true)) + ]), + E('pre', { + 'class': 'wifi-qr-code-content', + 'style': 'flex:1; overflow:auto; word-break:break-all;', + 'click': function(ev) { + let sel = window.getSelection(); + let range = document.createRange(); + + range.selectNodeContents(ev.currentTarget); + + sel.removeAllRanges(); + sel.addRange(range); + } + }, [wifi_qr]) + ]); + }; + + return qrm.render().then(function(nodes) { + let dStyle = mapNode.style; + mapNode.style.display = 'none'; + + let bRowStyle = mapNode.nextElementSibling.style; + mapNode.nextElementSibling.style.display = 'none'; + + headNode.appendChild(E('span', [' » ', _('Generate guest WiFi QR…')])); + mapNode.parentNode.appendChild(E([], [ + nodes, + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn', + 'click': function() { + nodes.parentNode.removeChild(nodes.nextSibling); + nodes.parentNode.removeChild(nodes); + mapNode.style = dStyle; + mapNode.nextSibling.style = bRowStyle; + headNode.removeChild(headNode.lastChild); + } + }, [_('Back to settings')]) + ]) + ])); + }); + }); + }; + + o.cfgvalue = function(section_id) { + return E('button', { + 'class': 'btn qr-code', + 'style': 'display:inline-flex;align-items:center;gap:.5em', + 'click': ui.createHandlerFn(this, 'handleGenerateQR', section_id) + }, [ + E(buildSVGQRCode('openwrt.org', null, { pixelSize: 1, ecc: 'L' }, true)), + _('Generate QR…') + ]); + }; + + o = s.option(form.Value, 'interface_ip', _('Interface IP address')); + o.datatype = 'ip4addr'; + o.rmempty = false; + o.default = DEFAULTS.ip; + o.cfgvalue = function(section_id) { + let val = uci.get('guestwifi', section_id, 'interface_ip'); + if (val) return val; + return randomFreeGuestIp(section_id) || DEFAULTS.ip; + }; + + o.textvalue = function(section_id) { + let val = this.cfgvalue(section_id) || this.default; + return E('code', {}, val); + }; + o.validate = function(section_id, value) { + if (!/^(\d{1,3}\.){3}\d{1,3}$/.test(value)) + return true; + + let used = usedInterfaceIps(section_id); + if (!used[value]) + return true; + + let newIp = randomFreeGuestIp(section_id); + if (!newIp) { + return _('The address %s is already in use and no free address is available in the 172.16.0.1 - 172.31.0.1 range.').format(value); + } + + let el = this.getUIElement(section_id); + if (el) el.setValue(newIp); + return true; + }; + + o = s.option(form.Value, 'netmask', _('Netmask')); + o.datatype = 'ip4addr'; + o.rmempty = false; + o.default = DEFAULTS.netmask; + o.modalonly = true; + + o = s.option(form.Value, 'dhcp_start', _('DHCP range start'), + _('Lowest leased address, as offset from the network address.')); + o.datatype = 'uinteger'; + o.rmempty = false; + o.default = DEFAULTS.dhcpStart; + o.modalonly = true; + + o = s.option(form.Value, 'dhcp_limit', _('DHCP client limit'), + _('Maximum number of leased addresses.')); + o.datatype = 'uinteger'; + o.rmempty = false; + o.default = DEFAULTS.dhcpLimit; + o.modalonly = true; + + o = s.option(form.Value, 'dhcp_lease', _('DHCP lease time'), + _('E.g. "1h", "30m", "12h". Minimum is "2m".')); + o.rmempty = false; + o.default = DEFAULTS.dhcpLease; + + o = s.option(form.ListValue, 'isolate', _('Client isolation'), + _('Blocks communication between clients on this guest network.')); + o.value('1', _('Yes')); + o.value('0', _('No')); + o.default = DEFAULTS.isolate; + o.rmempty = false; + o.modalonly = true; + + o = s.option(form.Value, 'macaddr', _('MAC address'), + _('Override default MAC address - the range of usable addresses might be limited by the driver')); + o.value('', _('driver default')); + o.value('random', _('randomly generated')); + o.datatype = "or('random',macaddr)"; + o.default = DEFAULTS.macaddr; + o.rmempty = true; + o.modalonly = true; + + o = s.option(widgets.ZoneSelect, 'fw_forward_dest', _('Forwarding destination zone'), + _('Firewall zone guest traffic is forwarded to (usually "wan"). Pick an existing zone from the list or fill out the -- custom -- field to enter one manually.')); + o.rmempty = false; + o.default = DEFAULTS.fwForwardDest; + o.modalonly = true; + + o = s.option(form.ListValue, 'fw_input', _('Input'), + _('Traffic from guests to the router itself (e.g. LuCI, SSH).')); + o.value('REJECT', _('reject')); o.value('ACCEPT', _('accept')); o.value('DROP', _('drop')); + o.default = DEFAULTS.fwInput; + o.rmempty = false; + o.modalonly = true; + + o = s.option(form.ListValue, 'fw_output', _('Output'), + _('Traffic from the router itself to guests. ACCEPT is recommended so router services (DHCP, DNS) keep working.')); + o.value('REJECT', _('reject')); o.value('ACCEPT', _('accept')); o.value('DROP', _('drop')); + o.default = DEFAULTS.fwOutput; + o.rmempty = false; + o.modalonly = true; + + o = s.option(form.ListValue, 'fw_forward', _('Forward'), + _('Traffic passing between guests and other networks/zones (e.g. LAN). Keep this REJECT or DROP to isolate guests from your LAN. ACCEPT would allow guests to reach other networks.')); + o.value('REJECT', _('reject')); o.value('ACCEPT', _('accept')); o.value('DROP', _('drop')); + o.default = DEFAULTS.fwForward; + o.rmempty = false; + o.modalonly = true; + + o = s.option(form.Value, 'fw_dhcp_ports', _('DHCP rule ports'), + _('UDP port range for DHCP traffic, e.g. "67-68".')); + o.rmempty = false; + o.default = DEFAULTS.fwDhcpPorts; + o.modalonly = true; + + o = s.option(form.Value, 'fw_dns_port', _('DNS rule port')); + o.datatype = 'port'; + o.rmempty = false; + o.default = DEFAULTS.fwDnsPort; + o.modalonly = true; + + return m.render().then(function(node) { + // LuCI theme often wins specificity over our injected + + + +
+ + +
+ +
+ + + + + + \ No newline at end of file diff --git a/luci-app-secbox/htdocs/luci-static/resources/view/secbox/overview.js b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/overview.js new file mode 100644 index 000000000..e6effc0ed --- /dev/null +++ b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/overview.js @@ -0,0 +1,486 @@ +'use strict'; +'require dom'; +'require view'; +'require poll'; +'require fs'; +'require ui'; +'require uci'; + +/* + service action handling +*/ +function handleAction(ev) { + ui.showIndicator('banip-action', _('Executing action: %s...').format(ev)); + return fs.exec_direct('/etc/init.d/banip', [ev]) + .then(function () { + ui.hideIndicator('banip-action'); + ui.addNotification(null, E('p', _('SecBox service action "%s" executed successfully.').format(ev)), 'info'); + }) + .catch(function (e) { + ui.hideIndicator('banip-action'); + ui.addNotification(null, E('p', _('Failed to execute action "%s": %s').format(ev, e.message)), 'error'); + }); +} + +function handleToggleService(enable) { + const action = enable ? 'start' : 'stop'; + const title = enable ? _('Enabling and starting SecBox service...') : _('Stopping and disabling SecBox service...'); + ui.showIndicator('banip-action', title); + return uci.load('banip').then(function () { + uci.set('banip', 'global', 'ban_enabled', enable ? '1' : '0'); + return uci.save(); + }).then(function () { + return uci.apply(); + }).then(function () { + return fs.exec_direct('/etc/init.d/banip', [enable ? 'restart' : 'stop']); + }).then(function () { + ui.hideIndicator('banip-action'); + ui.addNotification(null, E('p', enable ? _('SecBox service enabled and started.') : _('SecBox service stopped and disabled.')), 'info'); + setTimeout(function () { + location.reload(); + }, 1000); + }).catch(function (e) { + ui.hideIndicator('banip-action'); + ui.addNotification(null, E('p', _('Failed to update service status: %s').format(e.message)), 'error'); + }); +} + +/* + runtime string helpers +*/ +function parsePairs(text) { + const pairs = []; + (text || '').split(', ').forEach(function (item) { + const idx = item.indexOf(': '); + if (idx > 0) { + pairs.push([item.substring(0, idx), item.substring(idx + 2)]); + } else if (item.trim()) { + pairs.push([null, item.trim()]); + } + }); + return pairs; +} + +function pickValue(pairs, key) { + for (let i = 0; i < pairs.length; i++) { + if (pairs[i][0] === key) { + return pairs[i][1]; + } + } + return '-'; +} + +function expandPairs(pairs) { + const result = []; + pairs.forEach(function (pair) { + if (!pair[0]) { + result.push(pair); + return; + } + const group = pair[0].match(/^(.*?)\s*\(([^()]*\/[^()]*)\)$/); + if (group) { + const names = group[2].split('/'); + const values = pair[1].split('/'); + if (names.length === values.length) { + for (let i = 0; i < names.length; i++) { + result.push([group[1] + ' ' + names[i], values[i]]); + } + return; + } + } + result.push(pair); + }); + return result; +} + +function flagChips(text) { + const pairs = expandPairs(parsePairs(text)).filter(p => p[0]); + if (!pairs.length) return ['-']; + return pairs.map(function (flag) { + const on = flag[1] === '\u2714'; + return E('span', { + 'style': 'display: inline-block; margin: 0 .4em .35em 0; padding: .2em .55em; border-radius: 4px; font-size: .88em; white-space: nowrap;' + + (on ? 'background: rgba(16, 185, 129, 0.15); color: #059669; border: 1px solid rgba(16, 185, 129, 0.3);' : 'background: rgba(0, 0, 0, 0.05); color: #64748b; border: 1px solid rgba(0, 0, 0, 0.1);') + }, [ + (on ? '\u2714 ' : '\u2718 ') + flag[0] + ]); + }); +} + +function feedChips(feeds) { + const chips = (Array.isArray(feeds) ? feeds : []).filter(f => f && f !== '-'); + if (!chips.length) return ['-']; + return chips.map(function (feed) { + return E('span', { + 'style': 'display: inline-block; margin: 0 .4em .35em 0; padding: .2em .55em; border-radius: 4px; font-size: .88em; white-space: nowrap; background: rgba(37, 99, 235, 0.1); color: #2563eb; border: 1px solid rgba(37, 99, 235, 0.25);' + }, [feed]); + }); +} + +function sysPairs(text) { + let plain = 0; + return parsePairs(text).filter(function (pair) { + return pair[0] || ++plain === 1; + }); +} + +function splitCount(text) { + const value = text || ''; + const idx = value.indexOf(' ('); + if (idx < 0) { + return { 'count': value || '-', 'detail': '-' }; + } + return { + 'count': value.substring(0, idx) || '-', + 'detail': value.substring(idx + 2).replace(/\)$/, '') || '-' + }; +} + +function splitUplinks(list) { + const v4 = [], v6 = []; + (Array.isArray(list) ? list : []).forEach(function (addr) { + if (addr) { + (addr.indexOf(':') >= 0 ? v6 : v4).push(addr); + } + }); + return { + 'v4': v4.length ? v4.join(', ') : '-', + 'v6': v6.length ? v6.join(', ') : '-' + }; +} + +const keyMap = { + 'table': _('Table'), + 'priority': _('Priority'), + 'policy': _('Policy'), + 'proto 4': _('IPv4 Protocol'), + 'proto 6': _('IPv6 Protocol'), + 'limit icmp': _('ICMP Limit'), + 'limit syn': _('SYN Limit'), + 'limit udp': _('UDP Limit'), + 'cores': _('CPU Cores'), + 'log': _('Log Service'), + 'fetch': _('Download Tool'), + 'wan-dev': _('WAN Device'), + 'wan-if': _('WAN Interface'), + 'vlan-allow': _('Allowed VLAN'), + 'vlan-block': _('Blocked VLAN'), + 'uplink IPv4': _('Uplink IPv4'), + 'uplink IPv6': _('Uplink IPv6') +}; + +const stateMap = { + 'active': _('Running (active)'), + 'processing': _('Processing...'), + 'disabled': _('Disabled'), + 'error': _('Error'), + 'stopped': _('Stopped'), + 'not running': _('Not Running') +}; + +return view.extend({ + load: function () { + return Promise.all([ + uci.load('banip').catch(() => 0), + L.resolveDefault(fs.stat('/var/run/banIP/banIP.runtime.json'), null) + ]); + }, + + render: function (loadResult) { + let isEnabled = (uci.get('banip', 'global', 'ban_enabled') === '1'); + + const setNodes = (id, nodes) => { + const el = document.getElementById(id); + if (el) dom.content(el, nodes); + }; + + const setText = (id, value) => { + const el = document.getElementById(id); + if (el) el.textContent = value || '-'; + }; + + /* + poll runtime information + */ + let parseErrCount = 0; + poll.add(function () { + return L.resolveDefault(fs.stat('/var/run/banIP/banIP.runtime.json'), null).then(function (stat) { + if (!stat) return; + return Promise.all([ + L.resolveDefault(fs.read_direct('/var/run/banIP/banIP.runtime.json'), 'null'), + L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['actual']), '') + ]).then(function (results) { + const res = results[0]; + const actual = results[1]?.trim() || ''; + const toggleBtn = document.getElementById('btn-toggle-service'); + const dotEl = document.getElementById('status-dot'); + let info = null; + try { + info = JSON.parse(res); + parseErrCount = 0; + } catch (e) { + info = null; + parseErrCount++; + if (parseErrCount >= 3) { + ui.addNotification(null, E('p', _('Unable to parse the SecBox runtime information!')), 'error'); + poll.stop(); + } + return; + } + if (info) { + const rawState = info.status || '-'; + const isRunning = (rawState === 'active' || rawState === 'processing'); + setText('state_text', stateMap[rawState] || rawState); + setText('versions', `${info.frontend_ver || '-'} / ${info.backend_ver || '-'}`); + setNodes('actual', actual ? flagChips(actual) : ['-']); + + if (dotEl) { + if (rawState === 'active') { + dotEl.style.background = '#10b981'; + dotEl.style.boxShadow = '0 0 6px #10b981'; + } else if (rawState === 'processing') { + dotEl.style.background = '#3b82f6'; + dotEl.style.boxShadow = 'none'; + } else if (rawState === 'error') { + dotEl.style.background = '#ef4444'; + dotEl.style.boxShadow = 'none'; + } else { + dotEl.style.background = '#94a3b8'; + dotEl.style.boxShadow = 'none'; + } + } + + if (toggleBtn) { + if (isRunning) { + toggleBtn.textContent = _('Stop Service'); + toggleBtn.className = 'btn cbi-button cbi-button-reset'; + } else { + toggleBtn.textContent = _('Start Service'); + toggleBtn.className = 'btn cbi-button cbi-button-save'; + } + } + + const elements = splitCount(info.element_count); + const runPairs = parsePairs(info.last_run); + const join = list => (Array.isArray(list) && list.length) ? list.join(', ') : '-'; + + setText('elements_count', elements.count); + setText('elements_sub', elements.detail !== '-' ? elements.detail : ''); + setText('last_run_time', pickValue(runPairs, 'date / time')); + setText('last_run_detail', [pickValue(runPairs, 'mode'), pickValue(runPairs, 'duration'), pickValue(runPairs, 'memory')].filter(v => v && v !== '-').join(', ')); + + setText('wan_dev', join(info.wan_devices)); + setText('wan_if', join(info.wan_interfaces)); + setText('vlan_allow', join(info.vlan_allow)); + setText('vlan_block', join(info.vlan_block)); + + const uplinks = splitUplinks(info.active_uplink); + setText('uplink4', uplinks.v4); + setText('uplink6', uplinks.v6); + + // Render NFT pairs + const nftPairs = expandPairs(parsePairs(info.nft_info)); + const nftNodes = []; + nftPairs.forEach((p, idx) => { + if (p[0]) { + const k = keyMap[p[0].trim()] || p[0].trim(); + nftNodes.push(E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [ + E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, [k]), + E('td', { 'class': 'td cbi-section-table-cell op-col-val', 'style': 'font-family: monospace;' }, [p[1]]) + ])); + } + }); + setNodes('nft_tbody', nftNodes.length ? nftNodes : [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, ['-']), + E('td', { 'class': 'td cbi-section-table-cell op-col-val' }, ['-']) + ]) + ]); + + // System Info (Full-width row, 16% key : 84% val) + const sysList = sysPairs(info.system_info); + const sysNodes = []; + sysList.forEach((p, idx) => { + if (p[0]) { + const k = keyMap[p[0].trim()] || p[0].trim(); + sysNodes.push(E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [ + E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, [k]), + E('td', { 'class': 'td cbi-section-table-cell op-full-val' }, [p[1]]) + ])); + } else { + sysNodes.push(E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [ + E('td', { 'colspan': 2, 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important; color: #64748b; padding-left: .85em;' }, [p[1]]) + ])); + } + }); + setNodes('sys_tbody', sysNodes.length ? sysNodes : [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, ['-']), + E('td', { 'class': 'td cbi-section-table-cell op-full-val' }, ['-']) + ]) + ]); + + setNodes('feeds_container', feedChips(info.active_feeds)); + setNodes('flags_container', flagChips(info.run_flags)); + } + }); + }); + }, 3); + + const style = E('style', { 'type': 'text/css' }, [ + '#ban-op-grid .op-grid-2 { display: grid; gap: 1em; grid-template-columns: repeat(2, 1fr); margin-bottom: 1em; align-items: stretch; }' + + '@media (max-width: 900px) { #ban-op-grid .op-grid-2 { grid-template-columns: 1fr; } }' + + '#ban-op-grid .op-grid-1 { margin-bottom: 1em; width: 100%; }' + + '#ban-op-grid .op-card { background: var(--cbi-section-bg, #fff); border: 1px solid var(--cbi-border-color, #e5e7eb); border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; }' + + '#ban-op-grid .op-card-header { background: rgba(0, 0, 0, 0.025); min-height: 2.85em; height: 2.85em; padding: 0 1em; box-sizing: border-box; border-bottom: 1px solid var(--cbi-border-color, #e5e7eb); font-weight: 700; font-size: 1.02em; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }' + + '#ban-op-grid .op-card-body { padding: .85em 1em; flex: 1; display: flex; flex-direction: column; justify-content: center; }' + + '#ban-op-grid .op-card-body-table { padding: 0 !important; justify-content: flex-start; }' + + '#ban-op-grid .op-hero-val { font-size: 1.65em; font-weight: 700; line-height: 1.25; margin-bottom: .2em; }' + + '#ban-op-grid .op-hero-sub { font-size: .88em; color: #64748b; }' + + '#ban-op-grid table.table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; margin: 0; }' + + '#ban-op-grid table.table td { padding: .5em .85em; text-align: left !important; font-size: .95em; vertical-align: middle; }' + + '#ban-op-grid .op-col-key { width: 32% !important; max-width: 32% !important; min-width: 32% !important; font-weight: 600; text-align: left !important; word-break: break-word; }' + + '#ban-op-grid .op-col-val { width: 68% !important; text-align: left !important; word-break: break-all; }' + + '#ban-op-grid .op-full-key { width: 16% !important; max-width: 16% !important; min-width: 16% !important; font-weight: 600; text-align: left !important; word-break: break-word; }' + + '#ban-op-grid .op-full-val { width: 84% !important; text-align: left !important; word-break: break-word; }' + + '@media (max-width: 768px) { #ban-op-grid .op-full-key { width: 28% !important; min-width: 28% !important; } #ban-op-grid .op-full-val { width: 72% !important; } }' + + '@media (prefers-color-scheme: dark) {' + + '#ban-op-grid .op-card { background: rgba(255, 255, 255, 0.03); border-color: rgba(255, 255, 255, 0.12); }' + + '#ban-op-grid .op-card-header { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.12); }' + + '}' + ]); + + function ifRow(label, id, idx) { + return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [ + E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, [label]), + E('td', { 'class': 'td cbi-section-table-cell op-col-val', 'id': id }, ['-']) + ]); + } + + return E('div', { 'class': 'cbi-map', 'id': 'ban-op-grid' }, [ + style, + E('h2', { 'name': 'content' }, [_('SecBox Overview')]), + E('div', { 'class': 'cbi-map-descr' }, [ + _('Real-time runtime status, active sets and blocking statistics for the SecBox service.') + ]), + + /* Row 1: Strict 2 Columns (Status & Metrics) */ + E('div', { 'class': 'op-grid-2' }, [ + /* 1. Status */ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [ + _('Status'), + E('button', { + 'id': 'btn-toggle-service', + 'class': isEnabled ? 'btn cbi-button cbi-button-reset' : 'btn cbi-button cbi-button-save', + 'style': 'padding: .2em .65em; font-size: .88em; margin: 0;', + 'click': ui.createHandlerFn(this, function () { + return handleToggleService(!isEnabled); + }) + }, [isEnabled ? _('Stop Service') : _('Start Service')]) + ]), + E('div', { 'class': 'op-card-body' }, [ + E('div', { 'style': 'display: flex; align-items: center; gap: .5em; margin-bottom: .3em;' }, [ + E('span', { 'id': 'status-dot', 'style': 'width: .75em; height: .75em; border-radius: 50%; background: #94a3b8; display: inline-block;' }), + E('span', { 'class': 'op-hero-val', 'id': 'state_text', 'style': 'margin-bottom: 0;' }, ['-']) + ]), + E('div', { 'class': 'op-hero-sub' }, [ + _('Version'), ': ', E('span', { 'id': 'versions' }, ['-']) + ]), + E('div', { 'id': 'actual', 'style': 'margin-top: .5em;' }, ['-']) + ]) + ]), + + /* 2. Statistics & Run Info */ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('Rule Elements & Last Run')]), + E('div', { 'class': 'op-card-body' }, [ + E('div', { 'style': 'display: flex; justify-content: space-between; align-items: baseline;' }, [ + E('div', {}, [ + E('div', { 'class': 'op-hero-val', 'id': 'elements_count' }, ['-']), + E('div', { 'class': 'op-hero-sub', 'id': 'elements_sub' }, ['-']) + ]), + E('div', { 'style': 'text-align: right;' }, [ + E('div', { 'style': 'font-weight: 700; font-size: 1.15em;', 'id': 'last_run_time' }, ['-']), + E('div', { 'class': 'op-hero-sub', 'id': 'last_run_detail' }, ['-']) + ]) + ]) + ]) + ]) + ]), + + /* Row 2: Strict 2 Columns (NFT Info & Interfaces) */ + E('div', { 'class': 'op-grid-2' }, [ + /* NFT Information */ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('NFT Information')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + E('tbody', { 'id': 'nft_tbody' }, [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, ['-']), + E('td', { 'class': 'td cbi-section-table-cell op-col-val' }, ['-']) + ]) + ]) + ]) + ]) + ]), + + /* Interfaces */ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('Interfaces')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + ifRow(_('WAN Device'), 'wan_dev', 0), + ifRow(_('WAN Interface'), 'wan_if', 1), + ifRow(_('Allowed VLAN'), 'vlan_allow', 2), + ifRow(_('Blocked VLAN'), 'vlan_block', 3), + ifRow(_('Uplink IPv4'), 'uplink4', 4), + ifRow(_('Uplink IPv6'), 'uplink6', 5) + ]) + ]) + ]) + ]), + + /* Row 3: Full-Width Row (System Info) */ + E('div', { 'class': 'op-grid-1' }, [ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('System Info')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + E('tbody', { 'id': 'sys_tbody' }, [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, ['-']), + E('td', { 'class': 'td cbi-section-table-cell op-full-val' }, ['-']) + ]) + ]) + ]) + ]) + ]) + ]), + + /* Row 4: Full-Width Row (Active Feeds & Flags) */ + E('div', { 'class': 'op-grid-1' }, [ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('Active Feeds & Flags')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, [_('Active Feeds')]), + E('td', { 'class': 'td cbi-section-table-cell op-full-val', 'id': 'feeds_container' }, ['-']) + ]), + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-2' }, [ + E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, [_('Run Flags')]), + E('td', { 'class': 'td cbi-section-table-cell op-full-val', 'id': 'flags_container' }, ['-']) + ]) + ]) + ]) + ]) + ]) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-secbox/htdocs/luci-static/resources/view/secbox/processing_log.js b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/processing_log.js new file mode 100644 index 000000000..cc310fd7d --- /dev/null +++ b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/processing_log.js @@ -0,0 +1,4 @@ +'use strict'; +'require view.secbox.logtemplate as LogTemplate'; + +return LogTemplate.Logview('banIP-', _('Processing Log')); diff --git a/luci-app-secbox/htdocs/luci-static/resources/view/secbox/setreport.js b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/setreport.js new file mode 100644 index 000000000..cd20b9e0a --- /dev/null +++ b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/setreport.js @@ -0,0 +1,591 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; +'require uci'; + +/* + button handling +*/ +function handleAction(report, ev) { + if (ev === 'search') { + const triggerSearch = function () { + let searchInput = document.getElementById('search'); + let ip = searchInput ? searchInput.value.trim().toLowerCase() : ''; + + if (ip) { + window._lastSearchedIP = ip; + document.getElementById('result').textContent = _('Search is running, please wait...'); + if (window._banipPoller) { + clearInterval(window._banipPoller); + window._banipPoller = null; + } + L.resolveDefault(fs.write('/var/run/banIP/banIP.search', ''), '').then(function () { + L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['search', ip]), '').then(function () { + let attempts = 0; + window._banipPoller = setInterval(function () { + attempts++; + L.resolveDefault(fs.read('/var/run/banIP/banIP.search'), '').then(function (res) { + if (res && res.trim()) { + clearInterval(window._banipPoller); + window._banipPoller = null; + document.getElementById('result').textContent = res.trim(); + } else if (attempts >= 40) { + clearInterval(window._banipPoller); + window._banipPoller = null; + document.getElementById('result').textContent = _('Search timed out.'); + } + }); + }, 3000); + }); + }); + } + }; + + const appendToList = function (targetFile, listName) { + const ip = window._lastSearchedIP || (document.getElementById('search')?.value || '').trim(); + if (!ip) { + ui.addNotification(null, E('p', _('Please enter or search an IP first.')), 'warning'); + return; + } + return L.resolveDefault(fs.read_direct(targetFile), '').then(function (cur) { + const lines = cur.split('\n').map(l => l.trim()); + if (lines.includes(ip)) { + ui.addNotification(null, E('p', _('IP "%s" is already in %s.').format(ip, listName)), 'info'); + return; + } + const newContent = (cur.trim() ? cur.trim() + '\n' : '') + ip + '\n'; + return fs.write(targetFile, newContent).then(function () { + ui.addNotification(null, E('p', _('Successfully added "%s" to %s!').format(ip, listName)), 'info'); + }); + }).catch(function (e) { + ui.addNotification(null, E('p', _('Failed to update list: %s').format(e.message)), 'error'); + }); + }; + + ui.showModal(_('IP Search & Quick Action'), [ + E('p', _('Search the SecBox-related Sets for a specific IP and take quick actions.')), + E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column; margin-bottom:.5em;' }, [ + E('label', { 'style': 'padding-top:.5em', 'id': 'run' }, [ + E('input', { + 'class': 'cbi-input-text', + 'placeholder': '192.168.0.1 or domain', + 'style': 'width:300px; padding: 4px 8px;', + 'spellcheck': 'false', + 'id': 'search', + 'keydown': function (e) { + if (e.key === 'Enter') { + e.preventDefault(); + triggerSearch(); + } + } + }) + ]) + ]), + E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [ + E('h5', _('Result')), + E('textarea', { + 'id': 'result', + 'style': 'width: 100% !important; margin-top:.5em; padding: 6px; font-family: monospace; font-size:12px; border-radius:4px;', + 'readonly': 'readonly', + 'wrap': 'off', + 'rows': 15 + }) + ]), + E('div', { 'style': 'display:flex; justify-content:space-between; align-items:center; margin-top:1em;' }, [ + E('div', {}, [ + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'style': 'margin-right:.4em;', + 'click': ui.createHandlerFn(this, function () { + return appendToList('/etc/banip/banip.allowlist', _('Allowlist')); + }) + }, [_('+ Add to Allowlist')]), + E('button', { + 'class': 'btn cbi-button cbi-button-negative', + 'click': ui.createHandlerFn(this, function () { + return appendToList('/etc/banip/banip.blocklist', _('Blocklist')); + }) + }, [_('+ Add to Blocklist')]) + ]), + E('div', {}, [ + E('button', { + 'class': 'btn cbi-button', + 'style': 'margin-right:.4em;', + 'click': ui.hideModal + }, _('Close')), + E('button', { + 'class': 'btn cbi-button-positive important', + 'click': ui.createHandlerFn(this, function () { + triggerSearch(); + }) + }, _('Search IP')) + ]) + ]) + ]); + document.getElementById('search').focus(); + } + if (ev === 'content') { + let content, selectOption; + let errMsg = false; + + if (report[1]) { + try { + content = JSON.parse(report[1]); + } catch (e) { + content = ""; + if (!errMsg) { + errMsg = true; + return ui.addNotification(null, E('p', _('Unable to parse the ruleset file!')), 'error'); + } + } + } else { + return; + } + selectOption = [E('option', { value: '' }, [_('-- Set Selection --')])]; + Object.keys(content.nftables) + .filter(key => content.nftables[key].set?.name && content.nftables[key].set.table === 'SecBox') + .sort((a, b) => content.nftables[a].set.name.localeCompare(content.nftables[b].set.name)) + .forEach(key => { + selectOption.push(E('option', { 'value': content.nftables[key].set.name }, content.nftables[key].set.name)); + }); + ui.showModal(_('Set Content'), [ + E('p', _('List the elements of a specific SecBox-related Set.')), + E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [ + E('label', { 'class': 'cbi-input-select', 'style': 'padding-top:.5em', 'id': 'run' }, [ + E('h5', _('Set')), + E('select', { 'class': 'cbi-input-select', 'id': 'set' }, + selectOption + ) + ]), + ]), + E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [ + E('label', { 'class': 'cbi-checkbox', 'style': 'padding-top:.5em' }, [ + E('input', { + 'class': 'cbi-checkbox', + 'data-update': 'click change', + 'type': 'checkbox', + 'id': 'chkFilter', + 'disabled': 'disabled', + 'value': 'true' + }), + E('span', { 'style': 'margin-left: .5em;' }, _('Show only Set elements with hits')) + ]), + ]), + E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [ + E('h5', _('Result')), + E('textarea', { + 'id': 'result', + 'style': 'width: 100% !important; margin-top:.5em; padding: 5px; font-family: monospace', + 'readonly': 'readonly', + 'wrap': 'off', + 'rows': 20 + }) + ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn cbi-button', + 'style': 'float:none; margin-right:.4em;', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, function (ev) { + const checkbox = document.getElementById('chkFilter'); + const isChecked = checkbox.checked ? 'true' : 'false'; + let set = document.getElementById('set').value; + if (set) { + document.getElementById('result').textContent = 'Collecting Set content, please wait...'; + return L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['content', set, isChecked]), '').then(function (res) { + let result = document.getElementById('result'); + result.textContent = res ? res.trim() : _('Network error'); + document.getElementById('set').value = ''; + }); + } + document.getElementById('set').focus(); + }) + }, _('Show Content')) + ]) + ]); + if (uci.get('banip', 'global', 'ban_nftcount') === '1') { + const chk = document.querySelector('#chkFilter'); + if (chk) { + chk.removeAttribute('disabled'); + } + } + document.getElementById('set').focus(); + } + if (ev === 'map') { + const modal = ui.showModal(null, [ + E('div', { + id: 'mapModal', + style: 'position: relative;' + }, [ + E('iframe', { + id: 'mapFrame', + src: L.resource('view/secbox/map.html'), + style: 'width: 100%; height: 80vh; border: none;' + }), + ]), + E('div', { 'class': 'right' }, [ + E('button', { + 'class': 'btn cbi-button', + 'click': ui.createHandlerFn(this, function (ev) { + ui.hideModal(); + sessionStorage.clear(); + location.reload(); + }) + }, _('Cancel')), + ' ', + E('button', { + 'class': 'btn cbi-button-action', + 'click': ui.createHandlerFn(this, function (ev) { + let iframe = document.getElementById('mapFrame'); + iframe.contentWindow.location.reload(); + }) + }, _('Map Reset')) + ]) + ]); + modal.style.maxWidth = '90%'; + document.getElementById('mapModal').focus(); + } +} + +return view.extend({ + load: function () { + return Promise.all([ + L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['report', 'json']), '') + .then(function (res) { + if (res && res.trim()) return res; + return L.resolveDefault(fs.read_direct('/tmp/banIP-report/ban_report.jsn'), '') + .then(function (r) { + if (r && r.trim()) { + return L.resolveDefault(fs.read_direct('/tmp/banIP-report/ban_map.jsn'), '[]') + .then(function (m) { + let rObj = JSON.parse(r); + let mObj = JSON.parse(m); + return JSON.stringify([rObj[0] || rObj, mObj]); + }).catch(() => r); + } + return ''; + }); + }), + L.resolveDefault(fs.exec_direct('/usr/sbin/nft', ['-tj', 'list', 'sets']), ''), + uci.load('banip') + ]); + }, + + render: function (report) { + let content = [], rowSets, tblSets, notMsg; + + if (report) { + try { + content = JSON.parse(report[0]); + } catch (e) { + content[0] = ""; + } + } else { + content[0] = ""; + } + + function fmtCount(value) { + const text = String(value ?? '').trim(); + if (!/^\d+$/.test(text)) { + return text || '-'; + } + return text.replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + } + + function hitStats(sets) { + const stats = { 'sets': 0, 'counted': 0, 'total': 0, 'hits': [], 'worst': [] }; + + Object.keys(sets || {}).forEach(function (key) { + const set = sets[key]; + stats.sets++; + if (`${set.cnt_inbound ?? ''}${set.cnt_outbound ?? ''}` === '') { + return; + } + const count = (parseInt(set.cnt_inbound, 10) || 0) + (parseInt(set.cnt_outbound, 10) || 0); + const elements = parseInt(String(set.cnt_elements).replace(/\D/g, ''), 10) || 0; + stats.counted++; + stats.total += count; + if (count > 0) { + stats.hits.push({ 'name': key, 'elements': elements, 'hits': count, 'value': count }); + } + stats.worst.push({ 'name': key, 'elements': elements, 'hits': count, 'value': elements }); + }); + stats.hits.sort(function (a, b) { return b.value - a.value; }); + stats.worst.sort(function (a, b) { return (a.hits - b.hits) || (b.value - a.value); }); + return stats; + } + + function dirNode(direction, count, bold) { + const attrs = bold ? { 'style': 'font-weight: bold; text-align: left !important; word-break: break-all;' } : { 'style': 'text-align: left !important; word-break: break-all;' }; + if (!count) { + return E('em', attrs, [direction]); + } + return E('em', attrs, [ + direction, bold ? ' (' : ': (', + fmtCount(count), + ')' + ]); + } + + rowSets = []; + tblSets = E('table', { 'class': 'table cbi-section-table', 'id': 'sets', 'style': 'table-layout: fixed; width: 100%; word-break: break-all;' }, [ + E('tr', { 'class': 'tr cbi-section-table-titles' }, [ + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 15%; text-align: left !important;' }, [_('Set')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 10%; text-align: left !important;' }, [_('Count')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 12%; text-align: left !important;' }, [_('Inbound (packets)')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 12%; text-align: left !important;' }, [_('Outbound (packets)')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 13%; text-align: left !important;' }, [_('Port / Protocol')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 38%; text-align: left !important;' }, [_('Elements (max. 50)')]) + ]) + ]); + + if (content[0].sets) { + Object.keys(content[0].sets).sort().forEach(function (key, idx) { + rowSets.push([ + E('em', { 'style': 'text-align: left !important; word-break: break-all;' }, key), + E('em', { 'style': 'text-align: left !important;' }, fmtCount(content[0].sets[key].cnt_elements)), + dirNode(content[0].sets[key].inbound, content[0].sets[key].cnt_inbound, false), + dirNode(content[0].sets[key].outbound, content[0].sets[key].cnt_outbound, false), + E('em', { 'style': 'text-align: left !important;' }, content[0].sets[key].port), + E('em', { 'style': 'text-align: left !important; word-break: break-all; white-space: normal; display: block; overflow-wrap: anywhere;' }, content[0].sets[key].set_elements.join(", ")) + ]); + }); + rowSets.push([ + E('em', { 'style': 'font-weight: bold; text-align: left !important; word-break: break-all;' }, content[0].sum_sets), + E('em', { 'style': 'font-weight: bold; text-align: left !important;' }, fmtCount(content[0].sum_cntelements)), + dirNode(content[0].sum_setinbound, content[0].sum_cntinbound, true), + dirNode(content[0].sum_setoutbound, content[0].sum_cntoutbound, true), + E('em', { 'style': 'font-weight: bold; text-align: left !important;' }, content[0].sum_setports), + E('em', { 'style': 'font-weight: bold; text-align: left !important; word-break: break-all;' }, content[0].sum_setelements) + ]); + } + cbi_update_table(tblSets, rowSets); + + const sum = content?.[0] || {}; + const hits = hitStats(sum.sets); + + function renderStatRow(label, val, idx) { + return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [ + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 55% !important; max-width: 55% !important; min-width: 55% !important; text-align: left !important; font-weight: 600;' }, [label]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 45% !important; text-align: left !important; font-weight: bold;' }, [fmtCount(val)]) + ]); + } + + function renderTopCard(title, list) { + const rows = list.slice(0, 10).map(function (item, index) { + return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((index % 2) ? 2 : 1) }, [ + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 50% !important; text-align: left !important; font-family: monospace; word-break: break-all;' }, [item.name]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important;' }, [fmtCount(item.elements)]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important; font-weight: bold;' }, [fmtCount(item.hits)]) + ]); + }); + + return E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [title]), + E('div', { 'class': 'op-card-body', 'style': 'padding: 0;' }, [ + E('table', { 'class': 'table cbi-section-table', 'style': 'table-layout: fixed; width: 100%;' }, [ + E('tr', { 'class': 'tr cbi-section-table-titles' }, [ + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 50% !important; text-align: left !important;' }, [_('Set')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important;' }, [_('Elements')]), + E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important;' }, [_('Hits')]) + ]), + E('tbody', {}, rows.length ? rows : [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'colspan': 3, 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important; color: #888;' }, ['-']) + ]) + ]) + ]) + ]) + ]); + } + + const style = E('style', { 'type': 'text/css' }, [ + '#ban-report-grid .op-grid-2 { display: grid; gap: 1em; grid-template-columns: repeat(2, 1fr); margin-bottom: 1em; align-items: stretch; }' + + '@media (max-width: 900px) { #ban-report-grid .op-grid-2 { grid-template-columns: 1fr; } }' + + '#ban-report-grid .op-grid-1 { margin-bottom: 1em; width: 100%; }' + + '#ban-report-grid .op-card { background: var(--cbi-section-bg, #fff); border: 1px solid var(--cbi-border-color, #e5e7eb); border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; }' + + '#ban-report-grid .op-card-header { background: rgba(0, 0, 0, 0.025); min-height: 2.85em; height: 2.85em; padding: 0 1em; box-sizing: border-box; border-bottom: 1px solid var(--cbi-border-color, #e5e7eb); font-weight: 700; font-size: 1.02em; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }' + + '#ban-report-grid .op-card-body { padding: .85em 1em; flex: 1; display: flex; flex-direction: column; justify-content: center; }' + + '#ban-report-grid .op-card-body-table { padding: 0 !important; justify-content: flex-start; }' + + '#ban-report-grid table.table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; margin: 0; }' + + '#ban-report-grid table.table td { padding: .5em .85em; text-align: left !important; font-size: .95em; vertical-align: middle; }' + + '@media (prefers-color-scheme: dark) {' + + '#ban-report-grid .op-card { background: rgba(255, 255, 255, 0.03); border-color: rgba(255, 255, 255, 0.12); }' + + '#ban-report-grid .op-card-header { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.12); }' + + '}' + ]); + + const page = E('div', { 'class': 'cbi-map', 'id': 'ban-report-grid' }, [ + style, + E('h2', { 'name': 'content' }, [_('SecBox Set Reporting')]), + E('div', { 'class': 'cbi-map-descr' }, [ + _('This report shows the latest NFT Set statistics, press the \'Refresh\' button to get a new one. \ + You can also display the specific content of Sets, search for suspicious IPs and finally, these IPs can also be displayed on a map.') + ]), + + /* Row 1: Strict 2 Columns (Summary Overview & Auto-added) */ + E('div', { 'class': 'op-grid-2' }, [ + /* Overview */ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('Set Overview')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + renderStatRow(_('Sets'), sum.sum_sets, 0), + renderStatRow(_('Elements'), sum.sum_cntelements, 1), + renderStatRow(_('Timestamp'), sum.timestamp || '-', 2) + ]) + ]) + ]), + + /* Auto-added IPs */ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('Auto-added IPs')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table' }, [ + renderStatRow(_('Auto-added to Allowlist'), sum.autoadd_allow, 0), + renderStatRow(_('Auto-added to Blocklist'), sum.autoadd_block, 1) + ]) + ]) + ]) + ]), + + /* Row 2: Strict Full-Width (Blocked Packets 6 Metrics in 1 Full Card) */ + E('div', { 'class': 'op-grid-1' }, [ + E('div', { 'class': 'op-card' }, [ + E('div', { 'class': 'op-card-header' }, [_('Blocked Packets')]), + E('div', { 'class': 'op-card-body op-card-body-table' }, [ + E('table', { 'class': 'table cbi-section-table', 'style': 'table-layout: fixed; width: 100%;' }, [ + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('syn-flood')]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_synflood)]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('invalid ct')]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_ctinvalid)]) + ]), + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-2' }, [ + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('udp-flood')]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_udpflood)]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('invalid tcp')]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_tcpinvalid)]) + ]), + E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [ + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('icmp-flood')]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_icmpflood)]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('bcp38')]), + E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_bcp38)]) + ]) + ]) + ]) + ]) + ]), + + /* Row 3: Single Row Full-Width (Top Sets) */ + E('div', { 'class': 'op-grid-1' }, [ + renderTopCard(_('Top Sets (Most Hits)'), hits.hits) + ]), + + /* Row 4: Single Row Full-Width (Worst Sets) */ + E('div', { 'class': 'op-grid-1' }, [ + renderTopCard(_('Worst Sets (Least Hits)'), hits.worst) + ]), + + /* Bottom Full-Width Table */ + E('div', { 'class': 'cbi-section', 'style': 'overflow-x: auto;' }, [ + E('h3', _('Set details')), + E('div', { 'class': 'cbi-section-node' }, [ + tblSets + ]) + ]), + + /* Actions */ + E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': function () { + const btn = this; + document.querySelectorAll('.cbi-page-actions button').forEach(function (b) { + b.disabled = true; + }); + btn.blur(); + btn.classList.add('spinning'); + L.resolveDefault(fs.write('/var/run/banIP/banIP.report', ''), '').then(function () { + L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['report', 'gen']), ''); + let attempts = 0; + let poller = setInterval(function () { + L.resolveDefault(fs.read('/var/run/banIP/banIP.report'), '').then(function (res) { + res = (res || '').trim(); + if (res === '1') { + clearInterval(poller); + location.reload(); + } else if (res === '0') { + // polling + } else { + attempts++; + if (attempts >= 10) { + clearInterval(poller); + btn.classList.remove('spinning'); + document.querySelectorAll('.cbi-page-actions button').forEach(function (b) { + b.disabled = false; + }); + ui.addNotification(null, E('p', _('Failed to generate a SecBox report!')), 'error'); + } + } + }); + }, 3000); + }); + } + }, [_('Refresh')]), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, function () { + return handleAction(report, 'search'); + }) + }, [_('IP Search...')]), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, function () { + return handleAction(report, 'content'); + }) + }, [_('Set Content...')]), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'id': 'btnMap', + 'disabled': 'disabled', + 'click': ui.createHandlerFn(this, function () { + if (Array.isArray(content[1]) && content[1].length > 1) { + sessionStorage.setItem('mapData', JSON.stringify(content[1])); + return handleAction(report, 'map'); + } else { + if (!notMsg) { + notMsg = true; + return ui.addNotification(null, E('p', _('No GeoIP Map data!')), 'info'); + } + } + }) + }, [_('Map...')]) + ]) + ]); + + if (uci.get('banip', 'global', 'ban_nftcount') === '1' + && uci.get('banip', 'global', 'ban_map') === '1' + && (uci.get('banip', 'global', 'ban_allowlistonly') !== '1' + || (uci.get('banip', 'global', 'ban_feedin') || "").includes("allowlist") + || (uci.get('banip', 'global', 'ban_feedout') || "").includes("allowlist"))) { + const btn = page.querySelector('#btnMap'); + if (btn) { + btn.removeAttribute('disabled'); + } + } + return page; + }, + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-secbox/htdocs/luci-static/resources/view/secbox/settings.js b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/settings.js new file mode 100644 index 000000000..306ad6a95 --- /dev/null +++ b/luci-app-secbox/htdocs/luci-static/resources/view/secbox/settings.js @@ -0,0 +1,750 @@ +'use strict'; +'require dom'; +'require view'; +'require fs'; +'require ui'; +'require uci'; +'require form'; +'require tools.widgets as widgets'; + +/* + button handling +*/ +function handleAction(ev) { + if (ev === 'restart' || ev === 'reload') { + let map = document.querySelector('.cbi-map'); + return dom.callClassMethod(map, 'save') + .then(L.bind(ui.changes.apply, ui.changes)) + .then(function () { + ui.showIndicator('banip-action', _('Applying configuration & reloading SecBox...')); + return fs.exec_direct('/etc/init.d/banip', [ev]) + .then(function () { + ui.hideIndicator('banip-action'); + ui.addNotification(null, E('p', _('SecBox configuration saved and service %sed successfully.').format(ev)), 'info'); + }) + .catch(function (e) { + ui.hideIndicator('banip-action'); + ui.addNotification(null, E('p', _('Failed to %s SecBox: %s').format(ev, e.message)), 'error'); + }); + }); + } else { + return fs.exec_direct('/etc/init.d/banip', [ev]); + } +} + +return view.extend({ + load: function () { + return Promise.all([ + L.resolveDefault(fs.read_direct('/etc/banip/banip.custom.feeds'), ''), + L.resolveDefault(fs.read_direct('/etc/banip/banip.feeds'), ''), + L.resolveDefault(fs.read_direct('/etc/banip/banip.countries'), ''), + uci.load('banip').catch(() => 0) + ]); + }, + + render: function (result) { + /* + config check + */ + if (!result[3] || result[3].length === 0) { + ui.addNotification(null, E('p', _('No SecBox config found!')), 'error'); + return; + } + + let m, s, o; + const docLink = '' + _('online documentation') + ''; + const mapDescr = _('Configuration of the SecBox package to ban incoming and outgoing IPs via named nftables Sets. For further information please check the %s.').format(docLink); + m = new form.Map('banip', _('SecBox Settings'), mapDescr); + + /* + tabbed config section + */ + s = m.section(form.NamedSection, 'global', 'banip', _('Settings')); + s.addremove = false; + s.tab('general', _('General Settings')); + s.tab('advanced', _('Advanced Settings')); + s.tab('adv_chain', _('Table/Chain Settings')); + s.tab('adv_set', _('Feed/Set Settings')); + s.tab('adv_log', _('Log Settings')); + s.tab('adv_email', _('E-Mail Settings')); + s.tab('feeds', _('Feed Selection')); + + /* + general settings tab + */ + o = s.taboption('general', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('Changes on this tab need a SecBox service restart to take effect.') + '' + + '
'; + + o = s.taboption('general', form.Flag, 'ban_debug', _('Verbose Debug Logging'), _('Enable verbose debug logging in case of processing errors.')); + o.rmempty = false; + + o = s.taboption('general', form.Flag, 'ban_autodetect', _('Auto Detection'), _('Detect relevant network devices, interfaces, subnets, protocols and utilities automatically.')); + o.rmempty = false; + + o = s.taboption('general', form.Flag, 'ban_protov4', _('IPv4 Support'), _('Enables IPv4 support.')); + o.depends('ban_autodetect', '0'); + o.optional = true; + o.retain = true; + + o = s.taboption('general', form.Flag, 'ban_protov6', _('IPv6 Support'), _('Enables IPv6 support.')); + o.depends('ban_autodetect', '0'); + o.optional = true; + o.retain = true; + + o = s.taboption('general', widgets.DeviceSelect, 'ban_dev', _('Network Devices'), _('Select the WAN network device(s).')); + o.depends('ban_autodetect', '0'); + o.multiple = true; + o.nocreate = true; + o.optional = true; + o.retain = true; + + o = s.taboption('general', widgets.NetworkSelect, 'ban_ifv4', _('IPv4 Network Interfaces'), _('Select the logical WAN IPv4 network interface(s).')); + o.depends('ban_autodetect', '0'); + o.multiple = true; + o.nocreate = true; + o.optional = true; + o.retain = true; + + o = s.taboption('general', widgets.NetworkSelect, 'ban_ifv6', _('IPv6 Network Interfaces'), _('Select the logical WAN IPv6 network interface(s).')); + o.depends('ban_autodetect', '0'); + o.multiple = true; + o.nocreate = true; + o.optional = true; + o.retain = true; + + o = s.taboption('general', form.ListValue, 'ban_fetchcmd', _('Download Utility'), _('Select one of the pre-configured download utilities.')); + o.depends('ban_autodetect', '0'); + o.value('uclient-fetch'); + o.value('wget'); + o.value('curl'); + o.optional = true; + o.retain = true; + + o = s.taboption('general', form.Value, 'ban_fetchparm', _('Download Parameters'), _('Override the pre-configured download options for the selected download utility. The output flag, e.g. \'-o\' for curl or \'-O\' for wget, must be the last parameter.')); + o.depends('ban_autodetect', '0'); + o.optional = true; + o.retain = true; + + o = s.taboption('general', widgets.NetworkSelect, 'ban_trigger', _('Startup Trigger Interface'), _('List of available network interfaces to trigger the SecBox start.')); + o.multiple = true; + o.nocreate = true; + o.rmempty = true; + + o = s.taboption('general', form.Value, 'ban_triggerdelay', _('Trigger Delay'), _('Additional trigger delay in seconds before SecBox processing begins.')); + o.placeholder = '10'; + o.datatype = 'range(1,300)'; + o.rmempty = true; + + o = s.taboption('general', form.ListValue, 'ban_fetchretry', _('Download Retries'), _('Number of download attempts in case of an error (not supported by uclient-fetch).')); + o.value('1'); + o.value('3'); + o.value('5'); + o.value('10'); + o.value('20'); + o.default = '5'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('general', form.Flag, 'ban_fetchinsecure', _('Download Insecure'), _('Don\'t check SSL server certificates during download.')); + o.rmempty = true; + + o = s.taboption('general', form.Flag, 'ban_nftcount', _('Reporting Counters'), _('Enable NFT counters for Set elements and chain rules. Required for the GeoIP Map and packet statistics in the Set Reporting.')); + o.rmempty = true; + + o = s.taboption('general', form.Flag, 'ban_map', _('Enable GeoIP Map'), _('Enable a GeoIP Map with suspicious Set elements. This requires external requests to get the map tiles and geolocation data.')); + o.depends('ban_nftcount', '1'); + o.optional = true; + o.rmempty = true; + + /* + additional settings tab + */ + o = s.taboption('advanced', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('Changes on this tab needs a SecBox service restart to take effect.') + '' + + '
'; + + o = s.taboption('advanced', form.ListValue, 'ban_nicelimit', _('Nice Level'), _('The selected priority will be used for SecBox background processing.')); + o.value('-20', _('Highest Priority')); + o.value('-10', _('High Priority')); + o.value('0', _('Normal Priority')); + o.value('10', _('Less Priority')); + o.value('19', _('Least Priority')); + o.default = '0'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('advanced', form.ListValue, 'ban_filelimit', _('Max Open Files'), _('Increase the maximal number of open files, e.g. to handle the amount of temporary split files while loading the Sets.')); + o.value('512'); + o.value('1024'); + o.value('2048'); + o.value('4096'); + o.default = '1024'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('advanced', form.ListValue, 'ban_cores', _('CPU Cores'), _('Limit the cpu cores used by SecBox to save RAM, autodetected by default.')); + o.value('1'); + o.value('2'); + o.value('4'); + o.value('8'); + o.value('16'); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('advanced', form.ListValue, 'ban_splitsize', _('Set Split Size'), _('Split external Set loading after every n members to save RAM, disabled by default.')); + o.value('512'); + o.value('1024'); + o.value('2048'); + o.value('4096'); + o.value('8192'); + o.value('16384'); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('advanced', form.Value, 'ban_basedir', _('Base Directory'), _('Base working directory while SecBox processing.')); + o.placeholder = '/tmp'; + o.rmempty = true; + + o = s.taboption('advanced', form.Value, 'ban_backupdir', _('Backup Directory'), _('Target directory for compressed feed backups.')); + o.placeholder = '/tmp/SecBox-backup'; + o.rmempty = true; + + o = s.taboption('advanced', form.Value, 'ban_reportdir', _('Report Directory'), _('Target directory for SecBox-related report files.')); + o.placeholder = '/tmp/SecBox-report'; + o.rmempty = true; + + o = s.taboption('advanced', form.Value, 'ban_errordir', _('Error Directory'), _('Target directory for SecBox-related error files.')); + o.placeholder = '/tmp/SecBox-error'; + o.rmempty = true; + + o = s.taboption('advanced', form.Flag, 'ban_deduplicate', _('Deduplicate IPs'), _('Deduplicate IP addresses across all active Sets and tidy up the local blocklist.')); + o.default = 1; + o.rmempty = false; + + /* + advanced chain settings tab + */ + o = s.taboption('adv_chain', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('Changes on this tab needs a SecBox service restart to take effect.') + '' + + '
'; + + o = s.taboption('adv_chain', form.ListValue, 'ban_nftpriority', _('Chain Priority'), _('Set the NFT chain priority within the SecBox table, lower values means higher priority.')); + o.value('10'); + o.value('0'); + o.value('-100'); + o.value('-150'); + o.default = '-100'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_chain', form.Value, 'ban_allowflag', _('Allow Protocol/Ports'), _('Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-Input and WAN-Forward chain.')); + o.placeholder = 'tcp 80 443-445'; + o.rmempty = true; + + o = s.taboption('adv_chain', widgets.DeviceSelect, 'ban_vlanallow', _('Allow VLAN Forwards'), _('Always allow certain VLAN forwards.')); + o.multiple = true; + o.nocreate = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_chain', widgets.DeviceSelect, 'ban_vlanblock', _('Block VLAN Forwards'), _('Always block certain VLAN forwards.')); + o.multiple = true; + o.nocreate = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_chain', form.Flag, 'ban_bcp38', _('Enable BCP38'), _('Block packets with spoofed source IP addresses in all supported chains.')); + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_chain', form.ListValue, 'ban_icmplimit', _('ICMP-Threshold'), _('ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to \'0\'.')); + o.value('0'); + o.value('25'); + o.value('50'); + o.value('100'); + o.value('250'); + o.value('500'); + o.value('1000'); + o.default = '25'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_chain', form.ListValue, 'ban_synlimit', _('SYN-Threshold'), _('SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to \'0\'.')); + o.value('0'); + o.value('10'); + o.value('50'); + o.value('100'); + o.value('250'); + o.value('500'); + o.value('1000'); + o.default = '10'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_chain', form.ListValue, 'ban_udplimit', _('UDP-Threshold'), _('UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to \'0\'.')); + o.value('0'); + o.value('100'); + o.value('250'); + o.value('500'); + o.value('1000'); + o.value('2500'); + o.value('5000'); + o.default = '100'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + /* + advanced Set settings tab + */ + o = s.taboption('adv_set', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('Changes on this tab needs a SecBox service restart to take effect.') + '' + + '
'; + + o = s.taboption('adv_set', form.ListValue, 'ban_nftpolicy', _('Set Policy'), _('Set the NFT policy for SecBox-related Sets.')); + o.value('memory', _('memory')); + o.value('performance', _('performance')); + o.default = 'memory'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_set', form.ListValue, 'ban_nftretry', _('Set Load Retries'), _('Number of Set load attempts in case of an error.')); + o.value('1'); + o.value('3'); + o.value('5'); + o.default = '3'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_set', form.ListValue, 'ban_blockpolicy', _('Inbound Block Policy'), _('Drop packets silently or actively reject Inbound traffic.')); + o.value('drop', _('drop')); + o.value('reject', _('reject')); + o.default = 'drop'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + /* + feed parsing + */ + let feed, chain, descr; + let feeds = null; + if (result[0] && result[0].trim() !== "") { + try { + feeds = JSON.parse(result[0]); + } catch (e) { + ui.addNotification(null, E('p', _('Unable to parse the custom feed file!')), 'error'); + } + } + if (!feeds && result[1] && result[1].trim() !== "") { + try { + feeds = JSON.parse(result[1]); + } catch (e) { + ui.addNotification(null, E('p', _('Unable to parse the default feed file!')), 'error'); + } + } + + if (feeds && Object.keys(feeds).length) { + const feedKeys = Object.keys(feeds); + + o = s.taboption('adv_set', form.MultiValue, 'ban_feedin', _('Inbound Feed'), _('Override the default feed configuration and apply the feed to the inbound chain only.')); + o.value('allowlist', _('local allowlist')); + o.value('blocklist', _('local blocklist')); + feedKeys.forEach(f => o.value(f.trim())); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_set', form.MultiValue, 'ban_feedout', _('Outbound Feed'), _('Override the default feed configuration and apply the feed to the outbound chain only.')); + o.value('allowlist', _('local allowlist')); + o.value('blocklist', _('local blocklist')); + feedKeys.forEach(f => o.value(f.trim())); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_set', form.MultiValue, 'ban_feedinout', _('Inbound & Outbound Feed'), _('Override the default feed configuration and apply the feed to the inbound and outbound chain.')); + o.value('allowlist', _('local allowlist')); + o.value('blocklist', _('local blocklist')); + feedKeys.forEach(f => o.value(f.trim())); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_set', form.MultiValue, 'ban_feedreset', _('Feed Flag Reset'), _('Override the default feed configuration and remove existing port/protocol limitations.')); + o.value('allowlist', _('local allowlist')); + o.value('blocklist', _('local blocklist')); + feedKeys.forEach(f => o.value(f.trim())); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_set', form.MultiValue, 'ban_feedcomplete', _('Feed Complete'), _('Opt out specific feeds from the deduplication process.')); + o.value('allowlist', _('local allowlist')); + o.value('blocklist', _('local blocklist')); + feedKeys.forEach(f => o.value(f.trim())); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + } + + /* + advanced log settings tab + */ + o = s.taboption('adv_log', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('Changes on this tab needs a SecBox service restart to take effect.') + '' + + '
'; + + o = s.taboption('adv_log', form.ListValue, 'ban_nftloglevel', _('NFT Log Level'), _('Set the syslog level for NFT logging.')); + o.value('emerg', _('emerg')); + o.value('alert', _('alert')); + o.value('crit', _('crit')); + o.value('err', _('err')); + o.value('warn', _('warn')); + o.value('notice', _('notice')); + o.value('info', _('info')); + o.value('debug', _('debug')); + o.default = 'warn'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_log', form.Flag, 'ban_logprerouting', _('Log Prerouting'), _('Log suspicious packets in the Prerouting chain.')); + o.rmempty = false; + + o = s.taboption('adv_log', form.Flag, 'ban_loginbound', _('Log Inbound'), _('Log suspicious packets in the WAN-Input and WAN-Forward chain.')); + o.rmempty = false; + + o = s.taboption('adv_log', form.Flag, 'ban_logoutbound', _('Log Outbound'), _('Log suspicious packets in the LAN-Forward chain.')); + o.rmempty = false; + + o = s.taboption('adv_log', form.Value, 'ban_logreadfile', _('Logfile Location'), _('Location for parsing the log file, e.g. via syslog-ng, to deactivate the standard parsing via logread.')); + o.placeholder = '/var/log/messages'; + o.rmempty = true; + + o = s.taboption('adv_log', form.ListValue, 'ban_loglimit', _('Log Limit'), _('Parse only the last stated number of log entries for suspicious events. To disable the log monitor at all set it to \'0\'.')); + o.value('0'); + o.value('50'); + o.value('100'); + o.value('250'); + o.value('500'); + o.value('1000'); + o.default = '100'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_log', form.Value, 'ban_logcount', _('Log Count'), _('Number of failed login attempts of the same IP in the log before blocking.')); + o.placeholder = '1'; + o.datatype = 'range(1,10)'; + o.rmempty = true; + + o = s.taboption('adv_log', form.ListValue, 'ban_logratelimit', _('Log Rate Limit'), _('Rate (per second) for the shared NFT log limit, applied globally across all logged rules. Set to \'0\' to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers.')); + o.value('0'); + o.value('1'); + o.value('5'); + o.value('10'); + o.value('25'); + o.value('50'); + o.value('100'); + o.default = '10'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_log', form.ListValue, 'ban_logburstlimit', _('Log Burst Limit'), _('Burst size in packets for the shared NFT log limit.')); + o.depends({ ban_logratelimit: '0', '!reverse': true }); + o.value('1'); + o.value('5'); + o.value('10'); + o.value('25'); + o.value('50'); + o.default = '5'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_log', form.DynamicList, 'ban_logterm', _('Log Terms'), + _('Regular expressions to detect suspicious IPs in the system log. Uses the last IP per line; prefix a term with \'first:\' for source-first formats, e.g. nginx.')); + o.value('Exit before auth from', _('dropbear failed login')); + o.value('luci: failed login', _('LuCI failed login')); + o.value('error: maximum authentication attempts exceeded', _('sshd failed login')); + o.value('sshd.*Connection closed by.*\\[preauth\\]', _('sshd closed connection')); + o.value('SecurityEvent=\\"InvalidAccountID\\".*RemoteAddress=', _('asterisk invalid account')); + o.value('TLS Error: could not determine wrapping from \\[AF_INET\\]', _('openvpn TLS error')); + o.value('AdGuardHome.*\\[error\\].*/control/login: from ip', _('AdGuardHome login error')); + o.value('received a suspicious remote IP', _('Remote logging Event')); + o.placeholder = _('-- Please choose (optional) --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_log', form.Flag, 'ban_remotelog', _('Enable Remote Logging'), _('Enable the cgi interface to receive remote logging events.')); + o.default = 0; + o.optional = true; + o.rmempty = true; + + o = s.taboption('adv_log', form.Value, 'ban_remotetoken', _('Remote Token'), _('Token to communicate with the cgi interface.')); + o.depends('ban_remotelog', '1'); + o.datatype = 'and(minlength(3),maxlength(20))'; + o.validate = function (section_id, value) { + if (!value) { + return _('Empty field not allowed'); + } + if (!value.match(/^[A-Za-z0-9.:]+$/)) { + return _('Invalid characters'); + } + return true; + }; + o.optional = true; + o.rmempty = true; + + /* + advanced email settings tab + */ + o = s.taboption('adv_email', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('To enable email notifications, set up the \'msmtp\' package and specify a valid E-Mail receiver address.') + '' + + '
'; + + o = s.taboption('adv_email', form.Flag, 'ban_mailnotification', _('E-Mail Notification'), _('Receive E-Mail notifications with every SecBox run.')); + o.rmempty = true; + + o = s.taboption('adv_email', form.Value, 'ban_mailreceiver', _('E-Mail Receiver Address'), _('Receiver address for SecBox notification E-Mails, this information is required to enable E-Mail functionality.')); + o.placeholder = 'name@example.com'; + o.rmempty = true; + + o = s.taboption('adv_email', form.Value, 'ban_mailsender', _('E-Mail Sender Address'), _('Sender address for SecBox notification E-Mails.')); + o.placeholder = 'no-reply@SecBox'; + o.rmempty = true; + + o = s.taboption('adv_email', form.Value, 'ban_mailtopic', _('E-Mail Topic'), _('Topic for SecBox notification E-Mails.')); + o.placeholder = 'SecBox notification'; + o.rmempty = true; + + o = s.taboption('adv_email', form.Value, 'ban_mailprofile', _('E-Mail Profile'), _('Profile used by \'msmtp\' for SecBox notification E-Mails.')); + o.placeholder = 'ban_notify'; + o.datatype = 'uciname'; + o.rmempty = true; + + /* + feeds tab + */ + o = s.taboption('feeds', form.DummyValue, '_sub'); + o.rawhtml = true; + o.default = '' + _('Changes on this tab needs a SecBox service reload to take effect.') + '' + + '
' + + '' + _('External Blocklist Feeds') + ''; + + if (feeds && Object.keys(feeds).length) { + o = s.taboption('feeds', form.MultiValue, 'ban_feed', _('Blocklist Feed')); + for (let i = 0; i < Object.keys(feeds).length; i++) { + feed = (Object.keys(feeds)[i] || '').trim(); + chain = (feeds[feed]?.chain || 'in').trim(); + descr = (feeds[feed]?.descr || '-').trim(); + o.value(feed, feed + ' (' + chain + ', ' + descr + ')'); + } + o.placeholder = _('-- Please choose (optional) --'); + o.optional = true; + o.rmempty = true; + } + + o = s.taboption('feeds', form.DummyValue, '_feeds1'); + o.rawhtml = true; + o.default = '
' + _('Country Selection') + ''; + + let err, ccode, rir, country, countries = []; + if (result[2] && result[2].trim() !== "") { + countries = result[2].trim().split('\n'); + if (countries && countries.length) { + o = s.taboption('feeds', form.MultiValue, 'ban_country', _('Countries') + ' (RIR)'); + for (let i = 0; i < countries.length; i++) { + try { + ccode = countries[i].match(/^(\w+)\t/)[1].trim(); + rir = countries[i].match(/^\w+\t(\w+)\t/)[1].trim(); + country = countries[i].match(/^\w+\t\w+\t(.*$)/)[1].trim(); + o.value(ccode, country + ' (' + rir + ')'); + } catch (e) { + countries[i] = ""; + if (!err) { + ui.addNotification(null, E('p', _('Unable to parse the countries file!')), 'error'); + } + err = e; + } + } + o.placeholder = _('-- Please choose (optional) --'); + o.optional = true; + o.rmempty = true; + } + } + + o = s.taboption('feeds', form.MultiValue, 'ban_region', _('Regional Internet Registry'), _('Summary of countries based on the Regional Internet Registry (RIR).')); + o.value('AFRINIC', _('AFRINIC - serving Africa and the Indian Ocean region')); + o.value('APNIC', _('APNIC - serving the Asia Pacific region')); + o.value('ARIN', _('ARIN - serving Canada and the United States')); + o.value('LACNIC', _('LACNIC - serving the Latin American and Caribbean region')); + o.value('RIPE', _('RIPE - serving Europe, Middle East and Central Asia')); + o.placeholder = _('-- Please choose (optional) --'); + o.optional = true; + o.rmempty = true; + + o = s.taboption('feeds', form.Flag, 'ban_countrysplit', _('Split Country Set'), _('The selected Countries are stored in separate Sets.')); + o.rmempty = true; + + o = s.taboption('feeds', form.DummyValue, '_feeds2'); + o.rawhtml = true; + o.default = '
' + _('ASN Selection') + ''; + + o = s.taboption('feeds', form.DynamicList, 'ban_asn', _('ASNs'), _('Collection of IP addresses based on Autonomous System Numbers.')); + o.datatype = 'uinteger'; + o.optional = true; + o.rmempty = true; + + o = s.taboption('feeds', form.Flag, 'ban_asnsplit', _('Split ASN Set'), _('The selected ASNs are stored in separate Sets.')); + o.rmempty = true; + + o = s.taboption('feeds', form.DummyValue, '_feeds3'); + o.rawhtml = true; + o.default = '
' + _('External Allowlist Feeds') + ''; + + if (countries && countries.length) { + o = s.taboption('feeds', form.DynamicList, 'ban_allowurl', _('Allowlist Feed URLs')); + for (let i = 0; i < countries.length; i++) { + try { + ccode = countries[i].match(/^(\w+)\t/)[1].trim(); + rir = countries[i].match(/^\w+\t(\w+)\t/)[1].trim(); + country = countries[i].match(/^\w+\t\w+\t(.*$)/)[1].trim(); + o.value('https://www.ipdeny.com/ipblocks/data/aggregated/' + ccode + '-aggregated.zone', country + ' IPv4 (' + rir + ')'); + o.value('https://www.ipdeny.com/ipv6/ipaddresses/aggregated/' + ccode + '-aggregated.zone', country + ' IPv6 (' + rir + ')'); + } catch (e) { + countries[i] = ""; + } + } + o.placeholder = _('-- Please choose (optional) --'); + o.optional = true; + o.rmempty = true; + o.validate = function (section_id, value) { + if (!value) { + return true; + } + if (!value.match(/^(https?:\/\/)[A-Za-z0-9-]+\.[A-Za-z0-9.-]+(:[0-9]+)?(\/[A-Za-z0-9._\-?&+=:~#%]*)?$/)) { + return _('Invalid URL format'); + } + return true; + }; + } + + o = s.taboption('feeds', form.DummyValue, '_feeds4'); + o.rawhtml = true; + o.default = '
' + _('Local Feed Settings') + ''; + + o = s.taboption('feeds', form.Flag, 'ban_autoallowlist', _('Auto Allowlist'), _('Automatically add resolved domains and uplink IPs to the local SecBox allowlist.')); + o.default = 1; + o.rmempty = false; + + o = s.taboption('feeds', form.ListValue, 'ban_autoallowuplink', _('Auto Allow Uplink'), _('Limit the uplink autoallow function.')); + o.depends('ban_autoallowlist', '1'); + o.value('disable', _('Disable')); + o.value('subnet', _('Subnet')); + o.value('ip', _('IP')); + o.default = 'subnet'; + o.placeholder = _('-- default --'); + o.create = true; + o.optional = true; + o.rmempty = true; + + o = s.taboption('feeds', form.Flag, 'ban_autoblocklist', _('Auto Blocklist'), _('Automatically add resolved domains and suspicious IPs to the local SecBox blocklist.')); + o.default = 1; + o.rmempty = false; + + o = s.taboption('feeds', form.Flag, 'ban_autoblocksubnet', _('Auto Block Subnet'), _('Automatically add entire subnets to the blocklist Set based on an additional RDAP request with the suspicious IP.')); + o.default = 0; + o.optional = true; + o.rmempty = true; + + o = s.taboption('feeds', form.Value, 'ban_nftexpiry', _('Blocklist Set Expiry'), _('Expiry time for auto added blocklist Set members.')); + o.value('30ms'); + o.value('10s'); + o.value('1m'); + o.value('5m'); + o.value('1h'); + o.value('2h'); + o.value('1d'); + o.value('7d'); + o.value('2w'); + o.placeholder = _('-- default --'); + o.optional = true; + o.rmempty = true; + o.validate = function (section_id, value) { + if (!value) { + return true; + } + if (!value.match(/^([1-9][0-9]*(ms|s|m|h|d|w))+$/)) { + return _('Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m'); + } + return true; + }; + + o = s.taboption('feeds', form.Flag, 'ban_allowlistonly', _('Allowlist Only'), _('Restrict the internet access from/to a small number of secure IPs.')); + o.rmempty = false; + + s = m.section(form.NamedSection, 'global'); + s.render = L.bind(function () { + return E('div', { 'class': 'cbi-page-actions' }, [ + E('button', { + 'class': 'btn cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function () { + return handleAction('reload'); + }) + }, [_('Save & Reload')]), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-save', + 'click': ui.createHandlerFn(this, function () { + return handleAction('restart'); + }) + }, [_('Save & Restart')]), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-reset', + 'click': ui.createHandlerFn(this, function () { + return handleAction('stop'); + }) + }, [_('Stop')]) + ]); + }); + return m.render(); + }, + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-secbox/po/ar/secbox.po b/luci-app-secbox/po/ar/secbox.po new file mode 100644 index 000000000..5eb250611 --- /dev/null +++ b/luci-app-secbox/po/ar/secbox.po @@ -0,0 +1,1325 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-07 08:46+0000\n" +"Last-Translator: gaohaod \n" +"Language-Team: Arabic \n" +"Language: ar\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 2026.7.1.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "الإعدادات المتقدمة" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "مجلد النسخ الاحتياطي" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "إلغاء" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "الوصف" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "لا تتحقق من شهادات خادم SSL أثناء التنزيل." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "تنزيل غير آمن" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "تنزيل المعلمات" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "تحميل الأداة" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "إعلام البريد الإلكتروني" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "ملف تعريف البريد الإلكتروني" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "عنوان مستقبل البريد الإلكتروني" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "عنوان مرسل البريد الإلكتروني" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "موضوع البريد الإلكتروني" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "مفعَّل" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "الاعدادات العامة" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "معلومة" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "آخر تشغيل" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "نظرة عامة" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "نعش الذاكرة" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "دليل التقارير" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "نتيجة" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "تشغيل الإشارات" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "حفظ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "الاعدادات" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "بدء واجهة التشغيل" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "تأخير الزناد" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "تسجيل مطول للتصحيح" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/az/secbox.po b/luci-app-secbox/po/az/secbox.po new file mode 100644 index 000000000..9c4816d77 --- /dev/null +++ b/luci-app-secbox/po/az/secbox.po @@ -0,0 +1,1321 @@ +msgid "" +msgstr "" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: az\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/be/secbox.po b/luci-app-secbox/po/be/secbox.po new file mode 100644 index 000000000..394605ebf --- /dev/null +++ b/luci-app-secbox/po/be/secbox.po @@ -0,0 +1,1325 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-04-24 15:13+0000\n" +"Last-Translator: smallcms \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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" +"X-Generator: Weblate 5.17.1-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Уключана" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/bg/secbox.po b/luci-app-secbox/po/bg/secbox.po new file mode 100644 index 000000000..1bd3bd4ba --- /dev/null +++ b/luci-app-secbox/po/bg/secbox.po @@ -0,0 +1,1327 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-03-15 08:27+0000\n" +"Last-Translator: Daniel Konstantinov \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.17-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Моля, изберете (незадължително) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Избери --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- по подразбиране --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN-и" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Активни устройства" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Активни потоци" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Разширени настройки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Автоматичен блоклист" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Автоматично засичане" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Директоря за резервни копия" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Отказ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Държави" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Описание" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Не проверявай SSL сертификати по време на сваляне." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Изтегляне" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Сваляй несигурно" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Параметри за теглене" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "E-Mail Известяване" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-Mail Профил" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-Mail Изпращач" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-Mail Тема" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Включи banIP сервиз." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Включено" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Общи настройки" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 поддръжка" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Информация" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Запази" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Настройки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Състояние" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Спиране" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "Version" +#~ msgstr "Издание" diff --git a/luci-app-secbox/po/bn_BD/secbox.po b/luci-app-secbox/po/bn_BD/secbox.po new file mode 100644 index 000000000..0e0926ba9 --- /dev/null +++ b/luci-app-secbox/po/bn_BD/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-10-08 18:54+0000\n" +"Last-Translator: Rayhan Nabi \n" +"Language-Team: Bengali (Bangladesh) \n" +"Language: bn_BD\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.9-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "চালু ডিভাইসগুলো" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "বাতিল করুন" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "সক্রিয়" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "সাধারণ সেটিংস" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "সংরক্ষণ করুন" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "সেটিংস" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/ca/secbox.po b/luci-app-secbox/po/ca/secbox.po new file mode 100644 index 000000000..27b191737 --- /dev/null +++ b/luci-app-secbox/po/ca/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-05-30 00:53+0000\n" +"Last-Translator: Roger Pueyo Centelles \n" +"Language-Team: Catalan \n" +"Language: ca\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.6.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Configuració Avançada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Directori de còpies de seguretat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Canceŀla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Neteja" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Descripció" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Paràmetres de descàrrega" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Utilitat de baixades" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notificació de correu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Adreça del destinatari de correu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Activat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Paràmetres generals" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informació" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Darrera execució" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Visió de conjunt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Actualitza" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Desar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Paràmetres" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Estat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Enregistrament detallat de depuració" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "rebutja" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/cs/secbox.po b/luci-app-secbox/po/cs/secbox.po new file mode 100644 index 000000000..23a273644 --- /dev/null +++ b/luci-app-secbox/po/cs/secbox.po @@ -0,0 +1,1544 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-08-13 09:16+0000\n" +"Last-Translator: Pavel Borecki \n" +"Language-Team: Czech \n" +"Language: cs\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" +"X-Generator: Weblate 2026.9.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Vyberte (volitelné) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Nastavit výběr --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- výchozí --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC – obsluhuje africkou a indooceánskou oblast" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC – obsluhuje azijsko-pacifický region" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN – obsluhuje Kanadu a Spojené státy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Výběr ASN čísla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN čísla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktivní zařízení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Aktivní kanály" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktivní uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Chyba přihlášení AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "Dodatečné zpoždění (v sekundách) před začátkem zpracování banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Pokročilá nastavení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Povolit protokoly/porty" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Umožnit přeposílání VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URL adresy kanálu seznamu povolených" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Pouze seznam povolených" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Změny seznamu povolených byly uloženy – aby se změny projevily, načtěte " +"banIP znovu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Vždy umožňovat protokol (tcp/udp)s určitými porty nebo rozsahy portů v " +"řetězci WAN-Input a WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Vždy umožňovat určitá VLAN přeposílání." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Vždy blokovat určitá VLAN přeposílání." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Umožnit automatický uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automatický seznam povolených" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Automaticky blokovaná podsíť" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automatický seznam blokovaných" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatické zjištění" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Automaticky přidávat celé podsítě na seznam sadu seznamu blokovaných na " +"základě dalšího RDAP požadavku s podezřelou IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Automaticky přidávat přeložené domény a podezřelé IP adresy do lokálního " +"banIP seznamu blokovaných." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Automaticky přidávat IP adresy přeložených domén a uplinků do lokálního " +"banIP seznamu povolených." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Složka zálohy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Základní složka" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Základní pracovní složka při zpracovávání banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Blokovat VLAN přeposílání" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Blokovat pakety s podstrčenou zdrojovou IP adresou ve všech podporovaných " +"řetězcích." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Kanál seznamu blokovaných" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Skončení platnosti sady seznamu blokovaného" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Změny seznamu blokovaných byly uloženy – aby se změny projevily, načtěte " +"banIP znovu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Velikost dávky (v paketech) pro sdílený limit NFT záznamu událostí." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Jader procesoru" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Zrušit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Řetězec" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Priorita řetězce" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Aby se změny na tomto panelu projevily, je třeba službu banIP načíst znovu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Aby se projevily změny, provedené na tomto panelu, je třeba službu banIP " +"zrestartovat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Vyčistit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Sada IP adres založená na Číslech autonomních systémů." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Nastavení balíčku banIP pro blokování příchozích a odchozích IP adres " +"prostřednictvím sad pro nftables. Ohledně dalších informací se podívejte do " +"%s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Počet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Země" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Výběr země" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Editor uživatelsky určených kanálů" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Odstranit duplicity IP adres napříč všemi aktivními sadami a uklidit v " +"lokálním seznamu blokovaných." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Deduplikovat IP adresy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Popis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Automaticky zjišťovat relevantní síťová zařízení, rozhraní, podsítě, " +"protokoly a nástroje." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Zakázat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Při stahování nekontrolovat SSL certifikát serveru." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Stáhnout" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Stáhnout nezabezpečeně" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parametry stahování" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Opakované pokusy o stahování" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Nástroj pro stahování" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Zahazovat pakety bez informování o tom nebo aktivně odmítat příchozí provoz." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Upozornění e-mailem" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Profil e-mailu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Adresa příjemce e-mailu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Adresa odesilatele e-mailu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Nastavení e-mailu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Téma e-mailu" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Upravit seznam povolených" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Upravit seznam blokovaných" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Počet prvků" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Prvky (nejvýše 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Nevyplněná kolonka není povolena" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Povolit BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Povolit GeoIP mapu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Povolit NFT čítače pro prvky Set a pravidla řetězce. Vyžadováno pro GeoIP " +"mapu a statistiky paketů v nastavení hlášení." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Zapnout vzdálené zaznamenávání událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Zapnout GeoIP mapu s podezřelými prvky sady. Toto vyžaduje externí požadavky " +"pro získání mapových dlaždice a geolokačních dat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Zapnout službu banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" +"Pokud chcete obdržovat do záznamu událostí ty vzdálené, zapněte cgi rozhraní." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"V případě výskytu chyb zpracovávání zapnout zaznamenávání výřečných ladících " +"zpráv do záznamu událostí." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Povoleno" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Zapíná podporu IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Zapíná podporu IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Chybová složka" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "Čas skončení platnosti pro automaticky přidané členy sady blokovaných." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Externí kanály seznamu povolených" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Externí kanály seznamu blokovaných" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Nepodařilo se vytvořit výkaz banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Kanál dokončen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Reset příznaku kanálu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Název kanálu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Výběr kanálu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Nastavení kanálu/sady" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Vyplnit" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Záznamy událostí na bráně firewall" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Příznak" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Příznak nepodporován" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Obecná nastavení" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Udělit přístup k LuCI aplikaci banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Vysoká priorita" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Nejvyšší priorita" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "Práh ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Práh pro ICMP (v paketech/sekundu) pro zabránění útokům zahlcením na WAN. " +"Pokud chcete tuto pojistku vypnout, nastavte na „0“ (nula)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Hledat IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Hledat IP…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4 síťová rozhraní" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Podpora IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6 síťová rozhraní" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Podpora IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Dovnitř" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Příchozí a odchozí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Kanál příchozí a odchozí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Zásada blokování příchozího" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Příchozí kanál" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Příchozí (pakety)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Zvýšit maximální počet otevřených souborů, např. pro obsluhování množství " +"dočasných split souborů při načítání sad." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informace" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Neplatný formát URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Neplatné znaky" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Neplatný formát skončení platnosti, např. 5m, 2h, 1d nebo 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Neplatné vstupní hodnoty – změny není možné uložit." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC – obsluhuje latinskoamerický a karibský region" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Poslední spuštění" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Nejnižší priorita" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Nižší priorita" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Omezit počet jader procesoru, využívaných banIP a ušetřit tak kapacitu " +"operační paměti (ve výchozím stavu zjišťováno automaticky)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Omezit uplink funkci automatického povolování." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "Seznam rozhraní k dispozici pro spouštění startu banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Seznam prvků konkrétní sady související s banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Nastavení lokálního kanálu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Umístění pro zpracovávání souboru záznamu událostí, např. prostřednictvím " +"syslog-ng, pro deaktivaci standardního zpracovávání prostřednictvím logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Limit dávky záznamu událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Zaznamenávat počet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Zaznamenávat příchozí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Omezení záznamu událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Zaznamenávat odchozí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Zaznamenávat předsměrování" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Omezení četnosti záznamu událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Nastavení zaznamenávání událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Zaznamenávat podmínky" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Zaznamenávat podezřelé pakety v řetězci LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Zaznamenávat podezřelé pakety v řetězci Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Zaznamenávat podezřelé pakety v řetězci WAN-Input a WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Umístění souboru záznamu událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "Přihlášení do LuCI se nezdařilo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Reset mapy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Mapa…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Nejvýše otevřených souborů" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT informace" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Stupeň podrobnosti záznamu událostí v NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Síťová zařízení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Chyba sítě" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Priorita procesu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Zatím žádné záznamy událostí, související s %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Žádná data GeoIP mapy!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Nenalezeno žádné nastavení pro banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normální priorita" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Počet pokusů o načtení sady v případě chyby." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "Počet pokusů o stažení v případě chyby (nepodporováno uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Počet nezdařených pokusů o přihlášení ze stejné IP adresy v záznamu " +"událostí, po kterém začít blokovat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Vynechat konkrétní kanály z procesu odstraňování duplicit." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Odchozí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Odchozí kanál" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Odchozí (pakety)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Přebít výchozí nastavení kanálu a uplatnit kanál pro příchozí a odchozí " +"řetězce." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Přebít výchozí nastavení kanálu a uplatnit kanál pouze pro řetězec " +"příchozího." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Přebít výchozí nastavení kanálu a uplatnit kanál pouze pro řetězec odchozího." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Přebít výchozí nastavení kanálu a odebrat existující omezení portu/protokolu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Přebít přednastavené předvolby stahování pro vybraný nástroj pro stahování. " +"Je třeba, aby příznak výstupu, např. „-o“ pro curl nebo „-O“ pro wgent byl " +"uveden jako poslední parametr." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Přehled" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Ohledně podezřelých událostí zpracovávat pouze naposledy uvedený počet " +"položek záznamu událostí. Pokud chcete monitorování záznamu událostí zcela " +"vypnout, nastavte na „0“ (nula)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Port / Protokol" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Zpracovávání záznamu událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Profil používaný „msmtp“ pro notifikační e-maily z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE – obsluhuje Evropu, Střední východ s Střední Asii" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Četnost (za sekundu) pro sdílený limit NFT záznamu událostí, uplatňovaný " +"celkově napříč všemi zaznamenávanými pravidly. Pokud nechcete omezovat, " +"nastavte na „0“ (nula) – např. při používání ulogd nebo v případě jiných " +"nástrojů pro obsluhu záznamů událostí z uživatelského prostoru." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Při každém spuštění banIP obdržet upozornění e-mailem." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Adresa příjemce pro notifikační e-maily z banIP. Tento údaj je potřebný pro " +"zapnutí funkce E-mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Aktualizovat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Oblastní internetový registr" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Regulární výrazy pro zjišťování podezřelých IP adres v systémovém záznamu " +"událostí. Používá poslední IP adresu na řádku – pro formáty typu zdroj-první " +"(např. nginx) dejte před pojem 'first:'" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Vzdálený token" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Událost vzdáleného zaznamenávání událostí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Složka hlášení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Hlášení čítačů" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "Omezit přístup k Internetu z/na malý počet bezpečných IP adres." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Výsledek" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Pravidlo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Běhové příznaky" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Běhová informace" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "Práh SYN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Práh SYN (v paketech za sekundu) pro zabránění útokům zahlcením VAN. Pokud " +"chcete tuto pojistku vypnout, nastavte na „0“ (0)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Uložit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Uložit a načíst znovu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Uložit a restartovat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Hledat IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Probíhá hledání – vyčkejte…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Hledat konkrétní IP adresu v sadách souvisejících s banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Překročen časový limit na hledání." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Vyberte jeden z přednastavených nástrojů pro stahování." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Vyberte zařízení WAN sítě." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Vybrat logická WAN IPv4 síťová rozhraní." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Vybrat logická WAN IPv6 síťová rozhraní." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Adresa odesilatele pro e-maily s upozorňováními z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Sada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Nastavit obsah" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Nastavit obsah…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Opakovaných pokusů o načtení sady" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Nastavit zásadu" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Nastavit hlášení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Nastavit velikost rozdělení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Nastavit podrobnosti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Nastavit prioritu NFT řetězce v rámci tabulky banIP – nižší hodnoty " +"znamenají vyšší prioritu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Nastavit NFT zásadu pro sady související s banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Nastavit stupeň podrobnost zaznamenávání událostí do syslog pro NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Nastavení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Zobrazit obsah" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Zobrazit pouze prvky Nastavit, u kterých se strefilo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Rozdělit ASN sadu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Rozdělit sadu zemí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Rozdělit načítání externí sady po každých n členech a šetřit tak kapacitou " +"operační paměti – ve výchozím stavu vypnuto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Start spouštějící rozhraní" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Stav" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Zastavit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Podsíť" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "Souhrn zemí založený na Regionálním Internetovém registru (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Informace o systému" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Nastavení tabulky/řetězce" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Cílová složka pro soubory s chybami, souvisejícími s banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Cílová složka pro soubory hlášení související s banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Cílová složka pro komprimované zálohy kanálu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Seznam povolených je příliš velký – změny nebylo možné uložit." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Seznam blokovaných je příliš velký – změny nebylo možné uložit." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Vybrané ASN systémy jsou ukládány v oddělených sadách." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Označené země jsou ukládány v oddělených sadách." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"Vybraná priorita bude použita pro zpracovávání na pozadí v rámci banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "Výstup ze syslog – předfiltrované ohledně zpráv souvisejících s: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Toto je lokální banIP seznam povolených, který povoluje určité MAC, IP " +"adresy nebo doménová jména.
Prosím uvědomte si: " +"přidejte na každý řádek právě jednu MAC/IPv4/IPv6 adresu nebo doménový " +"název. Je možné použít i rozsahy (v CIDR zápisu) a navázání MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Toto je lokální banIP seznam vyloučených, který brání určitým MAC, IP " +"adresám nebo doménová jménům.
Prosím uvědomte si: " +"přidejte na každý řádek právě jednu MAC/IPv4/IPv6 adresu nebo doménový " +"název. Je možné použít i rozsahy (v CIDR zápisu) a navázání MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Toto hlášení zobrazuje nejnovější statistiky NFT sady – pro získání nového " +"klikněte na tlačítko „Načíst znovu“. Je také zobrazit si konkrétní obsah " +"sad, vyhledávat podezřelé IP adresy a dokonce je zobrazit mapě." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Časové razítko" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Aby bylo možné zapnout notifikace přes e-mail, nastavte balíček „msmtp“ a " +"zadejte platnou e-mailovou adresu příjemce." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token pro komunikaci s cgi rozhraním." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Téma pro notifikační e-maily z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Prodleva spuštění" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Práh UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Práh UDP (v paketech/sekundu) pro zabránění útokům zahlcením WAN. Pokud " +"chcete tuto pojistku vypnout, nastavte na „0“ (nula)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Nebylo možné zpracovat běhové informace o banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Nepodařilo se zpracovat soubor se zeměmi!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Nepodařilo se zpracovat soubor uživatelsky určeného kanálu!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Nepodařilo se zpracovat soubor výchozího kanálu!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Nepodařilo se zpracovat soubor se sadou pravidel!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Změny nebylo možné uložit: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Nahrát" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Nahrání souboru uživatelsky určeného kanálu se nezdařilo." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Podrobný záznam událostí pro ladění" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Pomocí tohoto editoru je možné nahrát soubor s vaším uživatelsky určeným " +"kanálem nebo naplnit ten počáteční (kopie 1:1 verze dodávané s tímto " +"balíčkem). Soubor se nachází v „etc/banip/banip.custom.feeds“. Poté tento " +"soubor můžete upravit, smazat položky, přidat nové nebo udělat lokální " +"zálohu. Pokud se chcete vrátit k verzi od správců balíčku, stačí jen " +"vyčistit soubor s uživatelsky určeným kanálem." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "výstraha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "asterisk – neplatný účet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "automaticky přidané IP adresy do seznamu povolených" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "automaticky přidané IP adresy do seznamu vyloučených" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "blokované pakety bcp38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "blokované icmp-flood pakety" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "zablokovány neplatné ct pakety" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "zablokovány neplatné tcp pakety" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "blokované syn-flood pakety" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "blokované udp-flood pakety" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "krit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "ladění" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "zahodit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "dropbear – nepodařilo se přihlásit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "nouzov" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "chyb" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "informace" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lokální seznam povolených" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lokální seznam blokovaných" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "paměť" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "upozornění" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "online dokumentace" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "openvpn – chyba TLS" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "výkon" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "odmítnout" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd uzavřel spojení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "sshd nezdařené přihlášení" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "varov." + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Regulární výrazy pro detekci podezřelých IP adres v systémovém záznamu " +#~ "událostí." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Přebít přednastavené předvolby pro stahování pro vybraný nástroj pro " +#~ "stahování." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Zapnout nft čítač pro každý prvek Set." + +#~ msgid "Set Element Counter" +#~ msgstr "Nastavit čítač prvků" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Nastavit prioritu nft řetězce v rámci tabulky banIP – nižší hodnoty " +#~ "znamenají vyšší priorita." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Nastavit nft zásadu pro sady související s banIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx – podezřelá IP" + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Invalid expiry format" +#~ msgstr "Neplatný formát skončení platnosti" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Nepodporovaný formát protokolu/URL" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Aby bylo možné zapnout e-mailové notifikace, nastavte balíček „msmpt“ a " +#~ "zadejte platnou adresu přijímače e-mailů." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Nebylo možné zpracovat běhové informace!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Nastavení balíčku banIP pro vylučování příchozích a odchozích IP adres " +#~ "prostřednictvím pojmenovaných nftables sad. Další informace viz dokumentace online" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", dílčí řetězec" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", concatinated" + +#~ msgid "NET" +#~ msgstr "SIT" + +#~ msgid "YOUR" +#~ msgstr "VASE" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Pravidlov4" + +#~ msgid "Rulev6" +#~ msgstr "Pravidlov6" + +#~ msgid "Version" +#~ msgstr "Verze" + +#~ msgid "logread not found on system." +#~ msgstr "nástroj logread nebyl v systému nalezen." + +#~ msgid "banIP firewall Log" +#~ msgstr "Záznam událostí brány firewall ohledně banIP" + +#~ msgid "Elements" +#~ msgstr "Prvky" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "GeoIP mapa není zapnutá!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Nepodařilo se zpracovat soubor s hlášením!" diff --git a/luci-app-secbox/po/da/secbox.po b/luci-app-secbox/po/da/secbox.po new file mode 100644 index 000000000..8fabfda77 --- /dev/null +++ b/luci-app-secbox/po/da/secbox.po @@ -0,0 +1,1329 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-01-25 02:53+0000\n" +"Last-Translator: drax red \n" +"Language-Team: Danish \n" +"Language: da\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.4-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN'er" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktive enheder" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Ekstra udløsningsforsinkelse i sekunder, før banIP-behandlingen begynder." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Avancerede indstillinger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatisk detektering" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Backup Mappe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Annuller" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Lande" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Beskrivelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Kontroller ikke SSL-servercertifikater under download." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Download usikker" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Download parametre" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Download hjælpeprogram" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Meddelelse pr. e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-mail profil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Modtagerens e-mailadresse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-mail afsenderadresse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-mail emne" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Aktiver banIP-tjenesten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Aktiveret" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Generelle indstillinger" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Giv adgang til LuCI-app banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Høj prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Højeste prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4-understøttelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6-understøttelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Information" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Sidste kørsel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Mindste prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Mindre prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Liste over tilgængelige netværks interfaces til at trigger banIP start." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Loggrænse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Log Vilkår" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normal prioritet (standard)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Oversigt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Profil, der anvendes af \"msmtp\" til banIP-meddelelses-e-mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Opdater" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Rapportmappe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Kør flag" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Kør oplysninger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Gem" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Afsenderadresse for banIP-meddelelses-e-mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Indstillinger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interface for opstartstrigger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Status" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Tidsstempel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Emne for banIP-meddelelses-e-mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Udløserforsinkelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Verbose Debug Logning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "Version" +#~ msgstr "Version" diff --git a/luci-app-secbox/po/de/secbox.po b/luci-app-secbox/po/de/secbox.po new file mode 100644 index 000000000..317129f78 --- /dev/null +++ b/luci-app-secbox/po/de/secbox.po @@ -0,0 +1,1546 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-04-21 11:00+0000\n" +"Last-Translator: Werner Schleifer \n" +"Language-Team: German \n" +"Language: de\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.17.1-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Bitte auswählen (optional) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Auswahl einstellen --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- Standard --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "Space>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - Afrika und der Region des Indischen Ozeans dienen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - der Region Asien-Pazifik dienen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - Kanada und den Vereinigten Staaten dienen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN-Auswahl" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "Autonome Systemnummern" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktive Geräte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Aktive Feeds bzw. Quellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktiver Uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "AdGuard Home Anmeldungsfehler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Zusätzliche Auslöseverzögerung in Sekunden, bevor die BanIP-Verarbeitung " +"beginnt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Erweiterte Einstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Protokoll/Ports zulassen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "VLAN Forwards erlauben" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Zulassungslisten-Feed-URLs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Nur Zulassungsliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Änderungen an der Zulassungsliste wurden gespeichert, bitte banIP neu laden, " +"um die Änderungen wirksam werden zu lassen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Immer ein Protokoll (tcp/udp) mit bestimmten Ports oder Portbereiche in der " +"WAN-Eingang- und WAN-Weiterleitung-Kette erlauben." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Immer bestimmte VLAN-Forwards erlauben." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Immer bestimmte VLAN-Forwards blockieren." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Uplink automatisch erlauben" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automatische Zulassungsliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Subnet automatisch blockieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automatische Sperrliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatische Erkennung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Automatisch dem Blocklist-Set ganze Subnetze auf Basis einer zusätzlichen " +"RDAP-Anforderung mit der verdächtigen IP hinzufügen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Aufgelöste Domains und verdächtige IPs der lokalen banIP-Sperrliste " +"automatisch hinzufügen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Der lokalen banIP-Zulassungsliste aufgelöste Domains und Uplink-IPs " +"automatisch hinzufügen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Backupverzeichnis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Basisverzeichnis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Basisarbeitsverzeichnis während der BanIP-Verarbeitung." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "VLAN-Weiterleitungen blockieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Blockierte Pakete mit spoofed Quell-IP-Adressen in allen unterstützten " +"Ketten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Sperrlisten-Feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Ablauf des Sperrlisten-Sets" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Blocklist-Änderungen wurden gespeichert, banIP neu laden, damit die " +"Änderungen wirksam werden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU-Kerne" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Abbrechen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Kette" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Kettenpriorität" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Änderungen auf dieser Registerkarte benötigen einen Neustart des banIP-" +"Services, um zu wirken." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Änderungen auf dieser Registerkarte benötigen einen Neustart des BanIP-" +"Services, um wirksam zu werden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Leeren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Sammlung von IP-Adressen basierend auf Autonomen Systemnummern." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Konfiguration des BanIP- Paket, um eingehende und ausgehende IPs über " +"benannte Nftables Sets zu verbieten. Weitere Informationen finden Sie in %." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Anzahl" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Länder" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Landesauswahl" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Benutzerdefinierter Feed-Editor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Deduplizieren von IP-Adressen in allen aktiven Sets und bereinigen der " +"lokalen Sperrliste." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "IPs deduplizieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Beschreibung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Automatische Erkennung relevanter Netzwerkgeräte, Schnittstellen, Subnetze, " +"Protokolle und Dienstprogramme." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Deaktivieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Während des Downloads keine SSL-Serverzertifikate überprüfen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Herunterladen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Unsicher herunterladen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Download Parameter" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Herunterladeversuche" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Download-Werkzeug" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Pakete still fallenlassen oder aktiv ablehnen bei eintreffendem Verkehr." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "E-Mail-Benachrichtigung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-Mail-Profil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "E-Mail Empfängeradresse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-Mail Absenderadresse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "E-Mail-Einstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-Mail-Thema" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Zulassungsliste bearbeiten" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Sperrliste bearbeiten" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Anzahl der Elemente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elemente (max. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Leeres Feld ist nicht erlaubt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "BCP38 aktivieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "GeoIP-Karte aktivieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Remote-Logging Aktivieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"GeoIP-Karte bei verdächtigen Set-Elementen aktivieren. Dies erfordert " +"externe Anfragen, um die Kartenfliesen und Geolokationsdaten zu erhalten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Aktiviere den banIP-Service." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" +"Aktivieren der cgi-Schnittstelle, um Remote-Logging-Ereignisse zu empfangen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Aktivieren ausführlicher Debug-Logs im Fehlerfall." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Aktiviert" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Aktiviert die IPv4-Unterstützung." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Aktiviert die IPv6-Unterstützung." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Fehlerverzeichnis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Verfallszeit für automatisch hinzugefügte Set-Mitglieder der Sperrliste." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Feeds externer Zulassungsliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Externe Sperrlisten-Feeds" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Versäumt, einen BanIP-Bericht zu erstellen!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Feed vollständig" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Feed Flag Reset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Feedname" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Feed-Auswahl" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Feed/Set Einstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Füllen" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Firewall-Protokoll" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Flag" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Flag nicht unterstützt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Allgemeine Einstellungen" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Zugriff auf LuCI-App banIP gewähren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Hohe Priorität" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Höchste Priorität" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-Schwelle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ICMP-Schwelle in Paketen pro Sekunde, um WAN-DoS-Angriffe zu verhindern. Um " +"diesen Schutz zu deaktivieren, auf '0' setzen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP-Suche" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP-Suche..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4-Netzwerkschnittstellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 Unterstützung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6-Netzwerkschnittstellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 Unterstützung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Eingehend" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Ein- und ausgehend" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Ein- und ausgehender Feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Block-Richtlinie eingehend" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Eingehender Feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Eingehend (Pakete)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Erhöhen Sie die maximale Anzahl von offenen Dateien, z.B. um die Anzahl der " +"temporären Split-Dateien beim Laden der Sets zu handhaben." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informationen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Invalides URL-Format" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Ungültige Zeichen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Ungültiges Ablaufformat, z.B. 5m, 2h, 1d oder 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Ungültige Eingangswerte, kann keine Änderungen speichern." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - für die lateinamerikanische und karibische Region" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Letzter Durchgang" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Niedrigste Priorität" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Niedrigere Priorität" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Beschränken der CPU-Kerne, die von banIP verwendet werden, um RAM zu sparen, " +"standardmäßig automatisch erkannt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Die Uplink Autoallow-Funktion einschränken." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Liste der verfügbaren Netzwerkschnittstellen, die den Start von banIP " +"auslösen können." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Auflistung der Elemente eines bestimmten banIP-bezogenen Sets." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Lokale Feed-Einstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Ort zum Parsen der Protokolldatei, z.B. über syslog-ng, um das " +"Standardparsing über logread zu deaktivieren." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Anzahl der Protokolle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Eingehende protokollieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Protokollbegrenzung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Ausgehende protokollieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Protokoll-Prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Protokolleinstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Protokollbedingungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Protokollieren verdächtiger Pakete in der LAN-Weiterleitungskette." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Protokollieren verdächtiger Pakete in der Prerouting-Kette." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" +"Protokollieren verdächtiger Pakete in den WAN-Eingangs- und WAN-" +"Weiterleitungsketten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Protokolldatei-Standort" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "LuCI konnte sich nicht anmelden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Karte zurücksetzen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Karte..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Max. geöffnete Dateien" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT-Informationen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "NFT-Protokollniveau" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Netzwerkadapter" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Netzwerkfehler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nice-Level" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Bisher noch keine %s bezogenen Protokolle!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Keine GeoIP-Kartendaten!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Keine BanIP config gefunden!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normale Priorität" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Anzahl der Set-Ladeversuche bei einem Fehler." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Anzahl der Downloadversuche bei einem Fehler (nicht von uclient-fetch " +"unterstützt)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Anzahl der fehlgeschlagenen Anmeldeversuche von der gleichen IP im Protokoll " +"vor der Sperrung." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Spezifische Feeds aus dem Deduplikationsprozess ausschließen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Ausgehend" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Ausgehender Feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Ausgehend (Pakete)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Überschreiben der voreingestellten Feed-Konfiguration und den Feed an die " +"eingehende und ausgehende Kette anwenden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Überschreiben der voreingestellten Feed-Konfiguration und den Feed nur an " +"die eingehende Kette anwenden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Überschreiben der voreingestellten Feed-Konfiguration und den Feed nur an " +"die ausgehende Kette anwenden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Überschreiben der voreingestellten Feed-Konfiguration und entfernen der " +"bestehenden Port/Protokoll-Beschränkungen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Übersicht" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Nur die zuletzt angegebene Anzahl von Protokoll-Einträgen für verdächtige " +"Ereignisse parsen. Um den Protokoll-Monitor zu deaktivieren, auf '0' setzen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Port / Protokoll" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Verarbeitungsprotokoll" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Von 'msmtp' verwendetes Profil für banIP-Benachrichtigungs-E-Mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - für Europa, Naher Osten und Zentralasien" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Erhalten Sie E-Mail-Benachrichtigungen für jeden banIP-Lauf." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Empfängeradresse für banIP-Benachrichtigungs-E-Mails. Diese Angabe ist " +"erforderlich, um die E-Mail-Funktionalität zu aktivieren." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Aktualisieren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Regionales Internetregister" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Entfernter Token" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Report-Verzeichnis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Beschränken des Internetzugangs von/zu einer kleinen Anzahl von sicheren IPs." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Ergebnis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Regel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Laufzeit-Flags" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informationen zur Ausführung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN-Schwelle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"SYN-Schwelle in Paketen pro Sekunde, um WAN-DoS Angriffe zu verhindern. Um " +"diesen Schutz zu deaktivieren, auf '0' setzen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Speichern" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Speichern und Neuladen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Speichern und Neustarten" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "IP suchen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Die Suche läuft, bitte warten..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Suche in den banIP-bezogenen Sets nach einer bestimmten IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Die Suche ist abgelaufen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Wählen Sie eines der vorkonfigurierten Download-Dienstprogramme." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Wählen Sie das/die WAN-Netzwerkgerät(e)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Wählen Sie die logische(n) WAN-IPv4-Netzwerkschnittstelle(n) aus." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Wählen Sie die logische(n) WAN-IPv6-Netzwerkschnittstelle(n) aus." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Absenderadresse für banIP-Benachrichtigungs-E-Mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Set" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Inhalt einstellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Inhalt einstellen..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Ladungswiederholungen einstellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Richtlinie einstellen" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Set-Berichte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Teilungsgröße festlegen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Details zum Set" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Legen Sie den Syslog-Level für die NFT-Protokollierung fest." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Einstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Inhalt anzeigen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Nur Set-Elemente mit Treffern anzeigen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "ASN-Set aufteilen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Länder-Set aufteilen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Laden des externen Sets nach n Mitgliedern aufteilen, um RAM zu speichern, " +"standardmäßig deaktiviert." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Trigger-Interface fürs Starten" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Status" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Stopp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Subnetz" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" +"Zusammenfassung von Ländern auf Grundlage des Regionalen Internetregisters " +"(RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Systeminfo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Tabellen-/Ketteneinstellungen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Zielverzeichnis für banIP-bezogene Fehlerdateien." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Zielverzeichnis für banIP-bezogene Berichtsdateien." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Zielverzeichnis für komprimierte Feed-Sicherungen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"Die Zulassungsliste ist zu groß, Änderungen können nicht gespeichert werden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" +"Die Sperrliste ist zu groß, Änderungen können nicht gespeichert werden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Die ausgewählten ASNs werden in separaten Sets gespeichert." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Die ausgewählten Länder werden in separaten Sets gespeichert." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"Die gewählte Priorität wird für die BanIP-Hintergrundverarbeitung verwendet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"Die Syslog-Ausgabe, vorgefiltert nach Meldungen, die sich auf Folgendes " +"beziehen: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Dies ist die lokale banIP-Zulassungsliste, die bestimmte MAC-, IP-Adressen " +"oder Domainnamen erlaubt.
Bitte beachten: Fügen Sie " +"nur genau eine MAC-/IPv4-/IPv6-Adresse oder einen Domänennamen pro Zeile " +"hinzu. Erlaubt sind Bereiche in CIDR-Notation und MAC/IP-Bindungen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Dies ist die lokale banIP-Sperrliste, die bestimmte MAC-, IP-Adressen oder " +"Domain-Namen verhindert.
Bitte beachten: Fügen Sie nur " +"genau eine MAC-/IPv4-/IPv6-Adresse oder einen Domänenamen pro Zeile hinzu. " +"Erlaubt sind Bereiche in CIDR-Notation und MAC/IP-Bindungen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Dieser Bericht zeigt die neuesten NFT-Set-Statistiken, drücken Sie die " +"Schaltfläche \"Aktualisieren\", um eine neue zu erhalten. Sie können auch " +"den spezifischen Inhalt von Sets anzeigen, nach verdächtigen IPs suchen und " +"schließlich können diese IPs auch auf einer Karte angezeigt werden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Zeitstempel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Um E-Mail-Benachrichtigungen zu aktivieren, richten Sie das Paket 'msmtp' " +"ein und geben Sie eine gültige E-Mail-Empfängeradresse an." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token, um mit der CGI-Schnittstelle zu kommunizieren." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Betreff für banIP-Benachrichtigungs-E-Mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Verzögerung der Trigger-Bedingung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP-Schwellenwert" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"UDP-Schwellenwert in Paketen pro Sekunde zur Verhinderung von WAN-DoS-" +"Angriffen. Um diese Schutzmaßnahme zu deaktivieren, setzen Sie den Wert auf " +"‚0‘." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Die banIP-Laufzeitinformationen konnten nicht analysiert werden!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Länderdatei kann nicht geparst werden!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Unfähig, die benutzerdefinierte Feed-Datei zu analysieren!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Unfähig, die Standard-Feed-Datei zu analysieren!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Regelsatzdatei kann nicht geparst werden!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Änderungen können nicht gespeichert werden: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Hochladen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Hochladen der benutzerdefinierten Feed-Datei gescheitert." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Ausführliche Debug-Protokollierung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Mit diesem Editor können Sie Ihre lokale benutzerdefinierte oder eine erste " +"Feed-Datei (eine 1:1 Kopie der mit dem Paket gelieferten) hochladen. Die " +"Datei befindet sich in '/etc/banip/banip.custom.feeds'. Dann können Sie " +"diese Datei bearbeiten, Einträge löschen, neue hinzufügen oder ein lokales " +"Backup erstellen. Um wieder auf die Version der Betreuer zu gehen, löschen " +"Sie einfach die benutzerdefinierte Feed-Datei." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "Alarm" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "ungültiges Asterisk-Konto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "automatisch hinzugefügte IPs auf die Zulassungsliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "automatisch hinzugefügte IPs auf die Sperrliste" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "blockierte bcp38 Pakete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "blockierte ICMP-flood-Pakete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "blockierte ungültige ct-Pakete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "blockierte ungültige TCP-Pakete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "blockierte SYN-flood-Pakete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "blockierte UDP-flood-Pakete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "debug" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "verwerfen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "dropbear konnte sich nicht anmelden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "emerg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "err" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lokale Zulassungsliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lokale Sperrliste" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "Speicher" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "notice" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "Online-Dokumentation" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "openvpn TLS-Fehler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "Leistung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "ablehnen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd geschlossene Verbindung" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "sshd konnte sich nicht anmelden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "warn" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Reguläre Ausdrücke, um verdächtige IPs im Systemprotokoll zu erkennen." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Überschreiben der vorkonfigurierten Download-Optionen für das ausgewählte " +#~ "Download-Dienstprogramm." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Nft-Zähler für jedes Set-Eement aktivieren." + +#~ msgid "Set Element Counter" +#~ msgstr "Elementzähler einstellen" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Die nft-Kettenpriorität innerhalb der banIP-Tabelle einstellen, " +#~ "niedrigere Werte bedeuten höhere Priorität." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Legen Sie die nft-Richtlinie für banIP-bezogene Sets fest." + +#~ msgid "" +#~ msgstr ">CSV-Separator>" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx verdächtige IP" + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Invalid expiry format" +#~ msgstr "Invalides Verfallsformat" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Protokoll/URL-Format nicht unterstützt" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Um E-Mail-Benachrichtigungen zu aktivieren, richten Sie das Paket " +#~ "\"msmtp\" ein und geben Sie eine gültige E-Mail-Empfängeradresse an." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Die Laufzeitinformationen konnten nicht verglichen werden!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Konfiguration des banIP-Pakets, um eingehende und ausgehende IPs über " +#~ "benannte nftables-Sets zu verbieten. Weitere Informationen finden Sie in " +#~ "der Online-Dokumentation" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", Teilzeichenkette" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", aneinandergehängt" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "IHR" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Version" + +#~ msgid "banIP Log" +#~ msgstr "banIP-Log" + +#~ msgid "banIP firewall Log" +#~ msgstr "banIP-Firewall-Log" + +#~ msgid "Elements" +#~ msgstr "Elemente" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "GeoIP-Karte ist nicht aktiviert!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Berichtsdatei kann nicht geparst werden!" diff --git a/luci-app-secbox/po/el/secbox.po b/luci-app-secbox/po/el/secbox.po new file mode 100644 index 000000000..946c9f54e --- /dev/null +++ b/luci-app-secbox/po/el/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2025-05-03 17:07+0000\n" +"Last-Translator: george kitsoukakis \n" +"Language-Team: Greek \n" +"Language: el\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.12-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "φάκελος διάσωσης" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Ακύρωση" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Περιγραφή" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Πληροφορίες" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Ρυθμίσεις" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/eo/secbox.po b/luci-app-secbox/po/eo/secbox.po new file mode 100644 index 000000000..f444b032a --- /dev/null +++ b/luci-app-secbox/po/eo/secbox.po @@ -0,0 +1,1321 @@ +msgid "" +msgstr "" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: eo\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/es/secbox.po b/luci-app-secbox/po/es/secbox.po new file mode 100644 index 000000000..ca7712085 --- /dev/null +++ b/luci-app-secbox/po/es/secbox.po @@ -0,0 +1,1575 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2019-07-23 22:17-0300\n" +"PO-Revision-Date: 2026-08-05 02:02+0000\n" +"Last-Translator: Franco Castillo \n" +"Language-Team: Spanish \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.8.1.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Por favor seleccione (opcional) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Selección de conjuntos --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- predeterminado --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - al servicio de África y la región del Océano Índico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - al servicio de la región de Asia Pacífico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - al servicio de Canadá y Estados Unidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Selección de ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Dispositivos activos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Fuentes activas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Enlace ascendente activo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Error de inicio de sesión de AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Retraso adicional de activación en segundos antes de que comience el " +"procesamiento de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Ajustes avanzados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Permitir protocolo/puertos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Permitir reenvíos de VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URLs de fuentes para la lista de permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Solo lista de permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Las modificaciones de la lista de permitidos se han guardado, recargue banIP " +"para que los cambios surtan efecto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Permite siempre un protocolo (TCP/UDP) con ciertos puertos o rangos de " +"puertos en la cadena de Entrada-WAN y Reenvío-WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Permite siempre ciertos reenvíos de VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Bloquea siempre ciertos reenvíos de VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Permitir automáticamente el enlace ascendente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Lista de permitidos automática" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Bloquear subred automáticamente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Lista de bloqueo automática" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Detección automática" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Agrega automáticamente subredes enteras al conjunto de lista de bloqueo " +"basándose en una solicitud RDAP adicional con la IP sospechosa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Agrega automáticamente dominios resueltos e IPs sospechosas a la lista de " +"bloqueo local de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Agrega automáticamente dominios resueltos e IPs de enlace ascendente a la " +"lista de permitidos local de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Directorio de respaldo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Directorio base" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Directorio de trabajo base durante el procesamiento de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Bloquear reenvíos de VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Bloquear paquetes con direcciones IP de origen falsificadas en todas las " +"cadenas compatibles." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Fuente de lista de bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Expiración del conjunto de lista de bloqueo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Las modificaciones de la lista de bloqueo se han guardado, recarga banIP " +"para que los cambios surtan efecto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" +"Tamaño de ráfaga en paquetes para el límite de registro NFT compartido." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Núcleos de CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Cancelar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Cadena" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Prioridad de cadena" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Los cambios en esta pestaña necesitan una recarga del servicio banIP para " +"surtir efecto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Los cambios en esta pestaña necesitan un reinicio del servicio banIP para " +"surtir efecto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Limpiar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Direcciones IP basadas en Números de Sistema Autónomo (ASN)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Configuración del paquete banIP para bloquear direcciones IP entrantes y " +"salientes mediante conjuntos de nftables con nombre. Para más información, " +"consulte %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Contar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Países" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Selección de país" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Editor de fuente personalizada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Elimina duplicados de direcciones IP en todos los conjuntos activos y ordena " +"la lista de bloqueo local." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Eliminar duplicados de IPs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Descripción" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Detecta automáticamente dispositivos de red relevantes, interfaces, " +"subredes, protocolos y utilidades." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Desactivar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "No verifica los certificados SSL del servidor durante la descarga." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Descargar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Descarga insegura" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parámetros de descarga" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Reintentos de descarga" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Utilidad de descarga" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Descarta paquetes silenciosamente o rechaza activamente tráfico entrante." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notificación por correo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Perfil de correo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Dirección del destinatario del correo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Dirección del remitente del correo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Ajustes del correo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Asunto del correo" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Editar lista de permitidos" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Editar lista de bloqueo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Conteo de elementos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elementos (máx. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "No se permite el campo vacío" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Activar BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Activar mapa GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Activar contadores NFT para elementos de conjunto y reglas de cadena. " +"Requerido para el mapa GeoIP y las estadísticas de paquetes en el " +"«Configurar informes»." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Activar registro remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Activa un Mapa GeoIP con elementos de grupo sospechosos . Esto requiere " +"peticiones externas para obtener recuadros de mapa y datos de " +"geolocalización." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Activa el servicio banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Activa la interfaz CGI para recibir eventos de registro remoto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Activa registro detallado de depuración en caso de errores de procesamiento." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Activado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Activa soporte para IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Activa soporte para IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Directorio de errores" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Tiempo de expiración para miembros del conjunto de lista de bloqueo añadidos " +"automáticamente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Fuentes externas de listas de permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Fuentes de lista de bloqueo externas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "¡No se pudo generar un informe de banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Fuente completa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Restablecer bandera de fuente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Nombre de la fuente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Selección de fuente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Ajustes de fuente/conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Llenar" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Registro de Cortafuegos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Indicador" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Indicador no admitido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Ajustes generales" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Conceder acceso a la aplicación LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Alta prioridad" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Máxima prioridad" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "Umbral-ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Umbral de ICMP en paquetes por segundo para prevenir ataques WAN-DoS. Para " +"desactivar esta protección, configúrelo en ‘0’." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Búsqueda de IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Buscar IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Interfaces de red IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Soporte IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Interfaces de red IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Soporte IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Entrante" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Entrante y Saliente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Fuente Entrante y Saliente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Política de Bloqueo Entrante" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Fuente Entrante" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Entrante (paquetes)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Incrementa el número máximo de archivos abiertos, p. ej., para manejar la " +"cantidad de archivos temporales divididos mientras se cargan los conjuntos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Información" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Formato de URL no válido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Caracteres no válidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Formato de caducidad no válido, p. ej. 5m, 2h, 1d o 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Valores de entrada no válidos, no se puede guardar las modificaciones." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - al servicio de la región de América Latina y el Caribe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Última ejecución" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Mínima prioridad" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Menos prioridad" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Limita los núcleos de procesador usados por banIP para ahorrar RAM, " +"autodetectado por omisión." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Limita la función de auto-permitir en el enlace ascendente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Lista de interfaces de red disponibles para activar el inicio de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Enumera los elementos de un conjunto específico relacionado con banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Ajustes de fuente local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Ubicación para analizar el archivo de registro mediante syslog-ng, en lugar " +"del análisis estándar mediante logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Límite de ráfaga de registros" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Conteo de registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Registrar Entrante" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Límite de registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Registrar Saliente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Registrar enrutamiento previo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Límite de tasa de registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Ajustes de registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Términos de registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Registra paquetes sospechosos en la cadena reenvío-LAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Registra paquetes sospechosos en la cadena de pre-enrutamiento." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" +"Registra paquetes sospechosos en las cadenas Entrada-WAN y Reenvío-WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Ubicación de archivo de registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "Ingreso fallido a LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Restablecer mapa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Mapa..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Máximo de archivos abiertos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Información de NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Nivel de registro de NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Dispositivos de red" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Error de red" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Buen nivel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "¡Aún no hay registros relacionados con %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "¡No hay datos del mapa GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "¡No se encontró configuración banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Prioridad normal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Número de intentos de carga del conjunto en caso de un error." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Número de intentos de descarga en caso de error (no compatible con uclient-" +"fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Intentos de inicio de sesión fallidos de la misma IP en el registro antes de " +"bloquear." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Desuscribirse de fuentes específicas del proceso de deduplicación." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Saliente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Fuente Saliente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Saliente (paquetes)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Anular la configuración predeterminada de fuente y aplicar la fuente a la " +"cadena entrante y saliente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Anular la configuración predeterminada de fuente y aplicar la fuente solo a " +"la cadena entrante." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Anula la configuración predeterminada de fuente y aplica la fuente solo a la " +"cadena saliente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Anular la configuración predeterminada de fuente y eliminar limitaciones " +"existentes de puerto/protocolo." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Anula las opciones de descarga preconfiguradas para la utilidad de descarga " +"seleccionada. El indicador de salida, por ejemplo, «-o» para curl o «-O» " +"para wget, debe ser el último parámetro." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Vista general" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analiza solo el número indicado de últimas entradas de registro para eventos " +"sospechosos. Para desactivar el monitor de registros, configúrelo en ‘0’." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Puerto / Protocolo" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Registro de procesamiento" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Perfil utilizado por 'msmtp' para correos electrónicos de notificación de " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - sirviendo a Europa, Medio Oriente y Asia Central" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Tasa (por segundo) para el límite de registro compartido de NFT, aplicado " +"globalmente a todas las reglas registradas. Establezca en '0' para " +"desactivar completamente la limitación de tasa, por ejemplo, al usar ulogd u " +"otros gestores de registro en el espacio de usuario." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" +"Recibe notificaciones de correo electrónico con cada ejecución de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Dirección del destinatario para correos electrónicos de notificación de " +"banIP, esta información es necesaria para activar la funcionalidad de correo " +"electrónico." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Actualizar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Registro Regional de Internet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Expresiones regulares para detectar direcciones IP sospechosas en el " +"registro del sistema. Utilice la última IP por línea; anteponga «first:» a " +"un término para formatos con origen primero, por ejemplo, nginx." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Token remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Registro remoto de eventos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Directorio de Informes" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Contadores de informes" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Restringe el acceso a Internet desde/hacia un pequeño número de IPs seguras." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Regla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Indicadores de ejecución" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Información de ejecución" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "Umbral-SYN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Umbral-SYN en paquetes por segundo para prevenir ataques WAN-DoS. Para " +"desactivar esta protección, configúrelo en ‘0’." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Guardar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Guardar y Recargar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Guardar y reiniciar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Buscar IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "La búsqueda está en curso, por favor espere..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Busca una IP específica en los conjuntos relacionados con banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "La búsqueda ha caducado." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Selecciona una de las utilidades de descarga preconfiguradas." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Selecciona el(los) dispositivo(s) de red WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Selecciona la(s) interfaz(es) lógica(s) IPv4 de red WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Selecciona la(s) interfaz(es) lógica(s) IPv6 de red WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" +"Dirección del remitente para correos electrónicos de notificación de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Contenido del Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Contenido del Conjunto..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Reintentos de Carga de Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Política de Conjunto" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Informe de Conjuntos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Tamaño de División de Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Detalles de Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Establezca la prioridad de la cadena NFT dentro de la tabla banIP; los " +"valores más bajos indican una mayor prioridad." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Establezca la política NFT para los conjuntos relacionados con banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Establece el nivel de syslog para el registro de NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Ajustes" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Mostrar contenido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Mostrar solo elementos del conjunto con hits" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Dividir Conjunto de ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Dividir Conjunto de País" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Dividir la carga del conjunto externo después de cada n miembros para " +"ahorrar RAM, desactivado por defecto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interfaz de activación de inicio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Estado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Detener" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Subred" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "Resumen de países basado en el Registro Regional de Internet (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Info del sistema" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Ajustes de tabla/cadena" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Directorio destino para archivos de error relacionados con banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Directorio destino para archivos de informes relacionados con banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Directorio destino para respaldos comprimidos de fuentes." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"La lista de permitidos es muy grande, no se pueden guardar las " +"modificaciones." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" +"La lista de bloqueos es muy grande, no se pueden guardar las modificaciones." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Los ASNs seleccionados están almacenados en conjuntos separados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Los países seleccionados están almacenados en conjuntos separados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"La prioridad seleccionada se utilizará para el procesamiento en segundo " +"plano de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "La salida de syslog, prefiltrada para mensajes relacionados con: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Esta es la lista de permitidos local de banIP que permitirá ciertas " +"direcciones MAC, IP o nombres de dominio.
Por favor, tenga en " +"cuenta: añada solo una dirección MAC/IPv4/IPv6 o nombre de dominio " +"por línea. Se permiten rangos en notación CIDR y asociaciones MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Esta es la lista de bloqueos local de banIP que evitará ciertas direcciones " +"MAC, IP o nombres de dominio.
Por favor, tenga en cuenta: añada solo una dirección MAC/IPv4/IPv6 o nombre de dominio por línea. Se " +"permiten rangos en notación CIDR y asociaciones MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Este reporte muestra las estadísticas mas recientes del conjunto NFT, " +"presione el botón 'Refrescar' para obtener una nuevas. También puede mostrar " +"el contenido específico de conjuntos, busque IPs sospechosas y entonces " +"podrá mostrarlas también en el mapa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Marca de tiempo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Para activar las notificaciones por correo, configure el paquete «msmtp» y " +"especifique una dirección de correo válida para el destinatario." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token para comunicarse con la interfaz CGI." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Asunto para correos electrónicos de notificación de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Retraso de activación" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Umbral-UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Umbral-UDP en paquetes por segundo para prevenir ataques WAN-DoS. Para " +"desactivar esta protección, configúrelo en '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "¡No se puede analizar la información de tiempo de ejecución de banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "¡No se puede analizar el archivo de países!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "¡No se puede interpretar el archivo de fuente personalizada!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "¡No se puede interpretar el archivo de fuente predeterminado!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "¡No se puede analizar el archivo de reglas!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "No se pueden guardar las modificaciones: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Cargar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Falló la carga del archivo de fuente personalizada." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Registro de depuración detallado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Con este editor, puede subir su archivo personalizado de fuentes locales o " +"completar uno inicial (una copia 1:1 de la versión enviada con el paquete). " +"El archivo se encuentra en ‘/etc/banip/banip.custom.feeds’. Luego, puede " +"editar este archivo, eliminar entradas, agregar nuevas o hacer un respaldo " +"local. Para volver a la versión de los mantenedores, simplemente borre el " +"archivo personalizado de fuentes." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "alerta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "Cuenta de asterisk no válida" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "IPs agregadas automáticamente a la lista de permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "IPs agregadas automáticamente a la lista de bloqueados" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "paquetes bcp38 bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "paquetes ICMP-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "paquetes ct no válidos bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "paquetes TCP no válidos bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "paquetes syn-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "paquetes de udp-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crít" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "depurar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "descartar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "Ingreso fallido a dropbear" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "emerg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "err" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lista de permitidos local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lista de bloqueo local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "memoria" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "aviso" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "documentación en línea" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "Error TLS de openvpn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "rendimiento" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "rechazar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "Conexión sshd cerrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "Ingreso fallido sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "advertir" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Expresiones regulares para detectar IPs sospechosas en el sistema de " +#~ "registro." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Anula las opciones de descarga preconfiguradas para la utilidad de " +#~ "descarga seleccionada." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Activa contador nft para cada elemento de grupo." + +#~ msgid "Set Element Counter" +#~ msgstr "Contador de Elementos del Conjunto" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Establece la prioridad de la cadena nft dentro de la tabla banIP, valores " +#~ "más bajos significan mayor prioridad." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "" +#~ "Establece la política nft para los conjuntos relacionados con banIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "IP sospechosa de nginx" + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Invalid expiry format" +#~ msgstr "Formato de caducidad no válido" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "El formato de protocolo/URL no es soportado" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Para activar las notificaciones por correo electrónico, configure el " +#~ "paquete 'msmtp' y especifique una dirección de correo electrónico válida " +#~ "para el destinatario." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "¡No se puede analizar la información del tiempo de ejecución!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Configuración del paquete banIP para prohibir IP entrantes y salientes a " +#~ "través de conjuntos nombrados de nftables. Para más información, por " +#~ "favor consulte la documentación " +#~ "en línea" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", subcadena" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", concatenado" + +#~ msgid "NET" +#~ msgstr "RED" + +#~ msgid "YOUR" +#~ msgstr "TU" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Reglav4" + +#~ msgid "Rulev6" +#~ msgstr "Reglav6" + +#~ msgid "Version" +#~ msgstr "Versión" + +#~ msgid "logread not found on system." +#~ msgstr "logread no encontrado en el sistema." + +#~ msgid "banIP Log" +#~ msgstr "Registro de banIP" + +#~ msgid "banIP firewall Log" +#~ msgstr "Registro del cortafuegos banIP" + +#~ msgid "Elements" +#~ msgstr "Elementos" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "¡El mapa GeoIP no está activado!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "¡No se puede analizar el archivo de informe!" diff --git a/luci-app-secbox/po/et/secbox.po b/luci-app-secbox/po/et/secbox.po new file mode 100644 index 000000000..be4c0097a --- /dev/null +++ b/luci-app-secbox/po/et/secbox.po @@ -0,0 +1,1321 @@ +msgid "" +msgstr "" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: et\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/fa/secbox.po b/luci-app-secbox/po/fa/secbox.po new file mode 100644 index 000000000..f6121680f --- /dev/null +++ b/luci-app-secbox/po/fa/secbox.po @@ -0,0 +1,1366 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2025-12-20 14:35+0000\n" +"Last-Translator: Ashkan Azoun \n" +"Language-Team: Persian \n" +"Language: fa\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.15.1\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- لطفا انتخاب کنید (اختیاری) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- تنظیم انتخابی --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- پیش‌فرض --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "آفرینیک - خدمت به آفریقا و منطقه اقیانوس هند" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "آپنیک - خدمت به منطقه آسیا و اقیانوسیه" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "آرین - خدمت به کانادا و ایالات متحده" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "انتخاب خودگردان" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "خودگردان‌ها" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "دستگاه های فعال" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "فیدهای فعال" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "پیوند فعال" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "خطای ورود به AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "تنظیمات پیشرفته" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "اجازه دادن به پروتکل‌ها/پورت‌ها" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "اجازه دادن به انتقال VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "نشانی های فید لیست‌مجاز" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "فقط لیست‌مجاز" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"تغییرات در لیست‌اجازه ذخیره شده‌اند، مسدودیت آی‌‎پی را مجدداً بارگذاری کنید تا " +"تغییرات اعمال شوند." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"همیشه اجازه دادن به یک پروتکل (TCP/UDP) با پورت‌ها یا بازه‌های پورت مشخص در " +"زنجیره ورودی WAN و فوروارد WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "همیشه اجازه دادن به انتقال‌های مشخص VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "همیشه مسدود کردن انتقال‌های مشخص VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "پیوند مجاز خودکار" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "لیست‌مجاز خودکار" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "مسدود کردن خودکار زیرشبکه" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "لیست‌سیاه خودکار" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "شناسایی خودکار" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"به‌طور خودکار کل زیرشبکه‌ها را به لیست مسدود اضافه کنید، بر اساس یک درخواست " +"RDAP اضافی با IP مشکوک." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "دایرکتوری Base" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "زنجیره" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "جزئیات" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "غیرفعال کردن" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "فعال" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "تنظیمات عمومی" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "گزارش مسیر" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "اجرای پرچم‌ها" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "ذخیره" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "وضعیت" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "توقف" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "تاخیر در اجرا محرک" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "ثبت اشکال زدایی کامل" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr "، csv" + +#~ msgid ", substring" +#~ msgstr "، رشته فرعی" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr "، حذف تکرار" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr "، الحاق شده" + +#~ msgid "NET" +#~ msgstr "نت" + +#~ msgid "YOUR" +#~ msgstr "شما" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" diff --git a/luci-app-secbox/po/fi/secbox.po b/luci-app-secbox/po/fi/secbox.po new file mode 100644 index 000000000..bc86d30b6 --- /dev/null +++ b/luci-app-secbox/po/fi/secbox.po @@ -0,0 +1,1382 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-01-20 07:02+0000\n" +"Last-Translator: Marko Mäkelä \n" +"Language-Team: Finnish \n" +"Language: fi\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.16-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Valitse (valinnainen) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Aseta valinta --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- oletus --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - palvelee Afrikkaa ja Intian valtameren aluetta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - palvelee Aasian ja Tyynenmeren aluetta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - palvelee Kanadaa ja Yhdysvaltoja" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN-valinta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN:t" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktiiviset laitteet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Aktiiviset syötteet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktiivinen Uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "AdGuardHome-sisäänkirjautumisvirhe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Ylimääräinen käynnistysviive sekunteina ennen banIP-käsittelyn aloittamista." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Lisäasetukset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Salli protokolla/portit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Salli VLAN-edelleenvälitykset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Sallittujen syötteiden URL-osoitteet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Vain sallittujen lista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Sallittujen listan muutokset on tallennettu, lataa banIP uudelleen, jotta " +"muutokset tulevat voimaan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Salli aina protokolla (tcp/udp) tiettyjen porttien tai porttialueiden kanssa " +"WAN-tulo- ja WAN-välitysketjuissa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Salli aina tietyt VLAN-edelleenvälitykset." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Estä aina tietyt VLAN-edelleenvälitykset." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Salli Uplink automaattisesti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automaattinen sallittujen lista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Estä aliverkko automaattisesti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automaattinen estolista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automaattinen tunnistus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Lisää automaattisesti kokonaisia aliverkkoja estolistalle epäilyttävän IP-" +"osoitteen sisältävän RDAP-pyynnön perusteella." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Lisää ratkaistut verkkotunnukset ja epäilyttävät IP-osoitteet " +"automaattisesti paikalliseen banIP-estolistalle." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Lisää automaattisesti ratkaistut verkkotunnukset ja uplink-IP-osoitteet " +"paikalliseen banIP-sallittujen luetteloon." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Varmuuskopiohakemisto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Perushakemisto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Perustyöhakemisto banIP:n käsittelyn aikana." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Estä VLAN-edelleenvälitykset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Estolista-syöte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Estolistan umpeutumispäivämäärä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Estolistan muutokset on tallennettu, lataa banIP uudelleen, jotta muutokset " +"tulevat voimaan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU-ytimet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Peruuta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Ketju" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Ketjunprioriteetti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Tämän välilehden muutokset vaativat banIP-palvelun uudelleenlatauksen, jotta " +"ne tulevat voimaan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Tämän välilehden muutokset vaativat banIP-palvelun uudelleenkäynnistyksen, " +"jotta ne tulevat voimaan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Tyhjennä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" +"IP-osoitteiden kerääminen autonomisten järjestelmien numeroiden (Autonomous " +"System Numbers) perusteella." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Määrä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Maat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Maan valinta" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Mukautetun syötteen muokkain" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Poista IP-osoitteiden kaksoiskappaleet kaikista aktiivisista joukoista ja " +"siivoa paikallinen estolista." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Poista IP-osoitteiden kaksoiskappaleet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Kuvaus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Tunnistaa automaattisesti asiaankuuluvat verkkolaitteet, liitännät, " +"aliverkot, protokollat ja apuohjelmat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Poista käytöstä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Älä tarkista SSL-palvelinvarmenteita latauksen aikana." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Lataa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Lataa turvattomasti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Lataa parametrit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Latausten uudelleenyritykset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Lataustyökalu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Pudota paketteja hiljaisesti tai hylkää aktiivisesti sisääntuleva liikenne." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Sähköposti-ilmoitus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Sähköpostiprofiili" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Sähköpostin saajan osoite" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Sähköpostin lähettäjän osoite" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Sähköpostiasetukset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Sähköpostin aihe" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Muokkaa sallittujen luetteloa" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Muokkaa estolistaa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Elementtien määrä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elementit (enintään 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Tyhjä kenttä ei sallittu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Ota GeoIP Map käyttöön" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Ota etäkirjaus käyttöön" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Ota GeoIP Map käyttöön epäilyttävillä Set-elementeillä. Tämä vaatii ulkoisia " +"pyyntöjä karttaruutujen ja geolokaatiotietojen hakemiseksi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Ota banIP-palvelun käyttöön." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Ota cgi-liitäntä käyttöön etäkirjaustapahtumien vastaanottamiseksi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Ota yksityiskohtaisen viankorjauslokin kirjaus käyttöön käsittelyvirheiden " +"varalta." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Otettu käyttöön" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Ottaa IPv4-tuen käyttöön." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Ottaa IPv6-tuen käyttöön." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Virhehakemisto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Ulkoiset sallittujen listan syötteet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Ulkoiset estolistasyötteet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Syöte valmis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Syötteenlipun nollaus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Syötteen nimi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Syötteen valinta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Syöte/Joukkoasetukset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Täytä" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Palomuuriloki" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Lippu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Lippua ei tueta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Yleiset asetukset" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Korkea prioriteetti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Korkein prioriteetti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-kynnys" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ICMP-kynnysarvo paketteina sekunnissa WAN-DoS-hyökkäysten estämiseksi. Voit " +"poistaa tämän suojauksen käytöstä asettamalla sen '0':ksi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP-haku" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP-haku..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4-verkkoliitännät" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4-tuki" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6-verkkoliitännät" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6-tuki" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Sisääntuleva" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Sisääntuleva ja uloslähtevä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Sisääntulevan ja uloslähtevän syöte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Sisääntulevan liikenteen estokäytäntö" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Sisääntulevan syöte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Sisääntuleva (paketit)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Lisää avoimien tiedostojen enimmäismäärää, esim. käsitelläksesi " +"väliaikaisesti jaettujen tiedostojen määrän Joukkoja ladattaessa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Tiedot" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Virheellisiä merkkejä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Virheellisiä syöttöarvoja, muutoksia ei voi tallentaa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - palvelee Latinalaisen Amerikan ja Karibian aluetta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Viimeksi ajettu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Vähiten prioriteetti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Vähemmän prioriteetti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Rajoita banIP:n käyttämien suorittimen ytimien määrää RAM-muistin " +"säästämiseksi, oletuksena automaattisesti tunnistettu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Rajoita Uplinkin automaattisen sallimisen toimintoa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Luettelo käytettävissä olevista verkkoliitännöistä banIP:n käynnistyksen " +"laukaisemiseksi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Kirjaa sisääntuleva" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Ohita oletusarvoinen syötemääritys ja käytä syötettä sisääntulevan ja " +"uloslähtevän ketjuun." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Ohita oletusarvoinen syötemääritys ja käytä syötettä vain sisääntulevan " +"ketjuun." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Yleiskatsaus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Päivitä" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Tulos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Sääntö" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN-kynnysarvo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Montako SYN-pakettia sekunnissa tulkitaan WAN-DoS-hyökkäykseksi. Arvo '0' " +"poistaa tämän suojauksen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Tallenna" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Tallenna & lataa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Tallenna & käynnistä uudelleen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Etsi IP-osoitetta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Asetukset" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Ota nft-laskuri käyttöön jokaiselle Set-elementille." + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "banIP-paketin konfigurointi estämään saapuvat ja lähtevät IP-osoitteet " +#~ "nimettyjen nftables-sarjojen avulla. Lisätietoja on online-dokumentaatiossa" diff --git a/luci-app-secbox/po/fr/secbox.po b/luci-app-secbox/po/fr/secbox.po new file mode 100644 index 000000000..92cd59ef8 --- /dev/null +++ b/luci-app-secbox/po/fr/secbox.po @@ -0,0 +1,1404 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-04-29 11:03+0000\n" +"Last-Translator: sllk \n" +"Language-Team: French \n" +"Language: fr\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.17.1-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Veuillez choisir (facultatif) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Définir la sélection --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- par défaut --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "Les ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Appareils actifs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Délai de déclenchement supplémentaire en secondes avant le début du " +"traitement banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Paramètres avancés" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Détection automatique" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Ajoutez automatiquement des sous-réseaux entiers à l’ensemble de listes de " +"blocage en fonction d’une requête RDAP supplémentaire avec l’adresse IP " +"suspecte." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Ajoutez automatiquement les domaines résolus et les adresses IP suspectes à " +"la liste de blocage banIP locale." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Ajoutez automatiquement des domaines résolus et des adresses IP de liaison " +"montante à la liste d'autorisation banIP locale." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Répertoire de sauvegarde" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Annuler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Les modifications apportées à cet onglet nécessitent un redémarrage du " +"service banIP pour prendre effet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Pays" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Description" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Détecter automatiquement les périphériques, interfaces, sous-réseaux, " +"protocoles et utilitaires du réseau." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Désactivé" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" +"Ignorer la vérification des certificats serveur pendant le téléchargement." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Téléchargement non sécurisé" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Paramètres de téléchargement" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Télécharger l'utilitaire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notification par courriel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Courriel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Adresse courriel du destinataire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Adresse courriel de l'expéditeur" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Réglages e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Objet du courriel" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Activer le service banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Activé" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Réglages généraux" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Accorder l'accès à l'application LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Priorité élevée" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Priorité la plus élevée" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Prise en charge d’IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Prise en charge d’IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Augmenter le nombre maximal de fichiers ouverts, par exemple pour gérer le " +"nombre de fichiers temporaires divisés pendant le chargement des Sets." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Information" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Dernière exécution" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Priorité minimale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Moins prioritaire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Liste des interfaces réseau disponibles pour déclencher le démarrage du " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Limite de journalisation" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Conditions de journalisation" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Priorité normale (par défaut)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Nombre de tentatives de téléchargement en cas d’erreur (non pris en charge " +"par uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Nombre de tentatives de connexion infructueuses de la même IP dans le " +"journal avant le blocage." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Vue d'ensemble" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analyse uniquement le dernier nombre indiqué d'entrées du journal pour y " +"déceler des événements suspects. Pour désactiver le moniteur de journaux, " +"réglez-le à \"0\"." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Profil utilisé par 'msmtp' pour les courriel de notification de bannissement " +"IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Adresse de réception pour les e-mails de notification banIP, ces " +"informations sont nécessaires pour activer la fonctionnalité E-Mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Actualiser" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Répertoire des rapports" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Limitez l’accès à Internet depuis/vers un petit nombre d’adresses IP " +"sécurisées." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Résultat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Drapeaux d'exécution" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informations sur l’exécution" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Sauvegarder" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" +"Adresse de l'expéditeur des courriels de notification de bannissement IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Paramètres" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interface des déclencheurs de démarrage" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "État" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"La priorité sélectionnée sera utilisée pour le traitement en arrière-plan " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Horodatage" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Rubrique pour les courriels de notification banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Délai de déclenchement" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Journalisation détaillée du débogage" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "bannissement IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Remplacez les options de téléchargement préconfigurées pour l'utilitaire " +#~ "de téléchargement sélectionné." + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Pour activer les notifications par e-mail, configurez le package 'msmtp' " +#~ "et spécifiez une adresse de destinataire E-Mail valide." + +#~ msgid "" +#~ msgstr "" + +#, fuzzy +#~ msgid ", csv" +#~ msgstr ", csv" + +#, fuzzy +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "VOUS" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" diff --git a/luci-app-secbox/po/ga/secbox.po b/luci-app-secbox/po/ga/secbox.po new file mode 100644 index 000000000..1edfc503d --- /dev/null +++ b/luci-app-secbox/po/ga/secbox.po @@ -0,0 +1,1553 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-24 15:15+0000\n" +"Last-Translator: Aindriú Mac Giolla Eoin \n" +"Language-Team: Irish \n" +"Language: ga\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>" +"6 && n<11) ? 3 : 4;\n" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Roghnaigh le do thoil (roghnach) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "- Roghnú Socraigh -" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- réamhshocraithe --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - ag freastal ar an Afraic agus ar réigiún an Aigéan Indiach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - ag freastal ar réigiún na hÁise an Aigé" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - ag freastal ar Cheanada agus na Stáit Aontaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Roghnú ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNanna" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Feistí Gníomhacha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Fothaí Gníomhacha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Uplink Gníomhach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Earráid logáil isteach AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Moill bhreise ar spreagthóir i soicindí sula dtosaíonn próiseáil banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Socruithe Ardleibhéil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Ceadaigh Prótacal/Calafoirt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Ceadaigh VLAN ar aghaidh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URLanna Beatha Liosta Ceadaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Liosta Ceadaigh Amháin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Sábháiltear modhnuithe liosta, athluchtaigh BanIP go dtiocfaidh athruithe i " +"bhfeidhm." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Ceadaigh prótacal (tcp/udp) i gcónaí le calafoirt nó raonta calafoirt " +"áirithe i slabhra ionchur WAN agus WAN-forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Ceadaigh VLAN áirithe ar aghaidh i gcónaí." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Cuir bac ar aghaidh VLAN áirithe i gcónaí." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Cead Uathoibríoch Uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Liosta Ceaduithe Auto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Subnet Uathoibríoch Bloc" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Uathoibríoch Blocklist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Braite Auto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Cuir fo-líonta iomlána go huathoibríoch leis an mblocliosta Socraigh " +"bunaithe ar iarratas breise RDAP leis an IP amhrasach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Cuir fearainn réitithe agus IPanna amhrasacha go huathoibríoch leis an " +"mblocliosta áitiúil BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Cuir fearainn réitithe agus IPanna uplink go huathoibríoch leis an liosta " +"ceadanna áitiúil BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Eolaire Cúltaca" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Eolaire Bunúsach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Eolaire oibre bonn agus BanIP á phróiseáil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Bloc ar aghaidh VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Blocaigh paicéid le seoltaí IP foinseacha bréige i ngach slabhra tacaithe." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Fotha Blocliosta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Sraith Blocklist in éag" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Sábháiltear modhnuithe ar blocliosta, athluchtaigh BanIP go dtiocfaidh " +"athruithe i bhfeidhm." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Méid pléasctha i bpacáistí don teorainn loga NFT comhroinnte." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Croíleacáin LAP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Cealaigh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Slabhra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Tosaíocht Slabhra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Teastaíonn athlódáil seirbhíse banIP de bharr athruithe ar an gcluaisín seo " +"le go dtiocfaidh siad i bhfeidhm." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Teastaíonn athruithe ar an gcluaisín seo seirbhís BanIP atosú chun dul i " +"bhfeidhm." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Soiléir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Bailiúchán seoltaí IP bunaithe ar Uimhreacha Córais Uathrialacha." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Cumraíocht an phacáiste banIP chun IPanna isteach agus amach a thoirmeasc " +"trí shraitheanna nftables ainmnithe. Le haghaidh tuilleadh eolais, féach ar " +"%s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Áireamh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Tíortha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Roghnú Tíre" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Eagarthóir Fotha Saincheaptha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Déan seoltaí IP a dhúbailt ar fud gach tacar gníomhach agus slachtaigh an " +"blocliosta áitiúil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "IPanna a dhúpláil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Cur síos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Braith gléasanna líonra ábhartha, comhéadain, folíonta, prótacail agus " +"fóntais go huathoibríoch." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Díchumasaigh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Ná seiceáil deimhnithe freastalaí SSL le linn íoslódála." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Íoslódáil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Íoslódáil Insecure" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Paraiméadair Íoslódála" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Íoslódáil Ath-Iarrachtaí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Íoslódáil Utility" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "Buail paicéid go ciúin nó diúltaíonn go gníomhach do thrácht Isteach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Fógra Ríomhphoist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Próifíl R-phoist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Seoladh Glacadóra R-phoist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Seoladh Seoltóir R-phoist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Socruithe R-phoist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Ábhar ríomhphoist" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Cuir Liosta Ceadaithe in Eagar" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Cuir an Liosta Bloctha in Eagar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Líon eilimintí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Eilimintí (uasmhéid 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Ní cheadaítear réimse folamh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Cumasaigh BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Cumasaigh Léarscáil GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Cumasaigh cuntair NFT le haghaidh eilimintí Tacair agus rialacha slabhra. " +"Riachtanach don Léarscáil GeoIP agus do staitisticí paicéad sa Tuairisciú " +"Tacair." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Cumasaigh Logáil Cianda" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Cumasaigh Léarscáil GeoIP le heilimintí Set amhrasacha. Éilíonn sé seo " +"iarratais seachtracha chun na tíleanna léarscáile agus sonraí geolocation a " +"fháil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Cumasaigh an tseirbhís BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Cumasaigh an comhéadan cgi chun imeachtaí logála iargúlta a fháil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Cumasaigh logáil dífhabhtaithe fabhrach i gcás earráidí próiseála." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Cumasaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Cumasaíonn sé tacaíocht IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Cumasaíonn sé tacaíocht IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Eolaire Earráidí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "Am éaga do bhlocliosta uathoibríoch Socraigh baill." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Fothaí Liosta Ceaduithe Seach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Fothaí Blocliosta Seachtracha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Theip ar thuairisc banIP a ghiniúint!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Feed Críochnaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Athshocrú Brat Beatha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Ainm an Bheatha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Roghnú Beatha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Socruithe Beatha/Socraigh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Líon" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Logáil Balla dóiteáin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Bratach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Ní thacaítear le bratach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Socruithe Ginearálta" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Rochtain a dheonú ar aip LuCi BanIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Tosaíocht Ard" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Tosaíocht is Airde" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "Tairseach ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Tairseach ICMP i bpaicéid in aghaidh an tsoicind chun ionsaithe WAN-Dos a " +"chosc. Chun an cosaint seo a dhíchumasú socraigh é go '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Cuardach IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Cuardaigh IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Comhéadain Líonra IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Tacaíocht IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Comhéadain Líonra IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Tacaíocht IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "An trácht isteach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Isteach & Amach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Fotha Isteach & Amach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Beartas Bloc Isteach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Fotha Isteach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Isteach (paicéid)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Méadaigh an líon uasta comhaid oscailte, m.sh. chun méid na gcomhaid scoilte " +"sealadacha a láimhseáil agus na Tacair á luchtú." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Faisnéis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Formáid URL neamhbhailí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Carachtair neamhbhailí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Formáid éaga neamhbhailí, m.sh. 5m, 2u, 1l nó 1u30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Luachanna ionchuir neamhbhailí, ní féidir na modhnuithe a shábháil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - ag freastal ar réigiún Meiriceá Laidineach agus sa Mhu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Rith Deiridh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "An Tosaíocht is Lú" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Níos lú tosaíochta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Cuir teorainn leis na croíleacáin LAP a úsáideann banIP chun RAM a shábháil, " +"rud a bhraitear go huathoibríoch de réir réamhshocraithe." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Cuir teorainn leis an bhfeidhm autoallow uplink." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Liosta de na comhéadain líonra atá ar fáil chun tús a chur leis an banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Liostaigh eilimintí Sraith ar leith a bhaineann le BANIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Socruithe Beatha Áitiúla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Suíomh chun an comhad logála a pháirseáil, m.sh. trí syslog-ng, chun an " +"parsáil chaighdeánach a dhíghníomhachtú trí logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Teorainn Phléasctha Loga" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Líon Logáil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Logáil Isteach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Teorainn Logála" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Logáil Amach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Réamhshocrú Logáil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Teorainn Ráta Logála" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Socruithe Logála" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Téarmaí Logáil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Logáil paicéid amhrasacha sa slabhra LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Logáil paicéid amhrasacha sa slabhra Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Logáil paicéid amhrasacha sa slabhra WAN-Input agus WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Suíomh Logfile" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "Theip ar logáil isteach LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Athshocrú Léarscáile" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Léarscáil..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Comhaid Oscailte Max" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Faisnéis NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Leibhéal Logála NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Gléasanna Líonra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Earráid líonra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Leibhéal Deas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Níl aon logaí gaolmhara le %s fós!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Gan aon sonraí Léarscáile GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Níor aimsíodh aon chumraíocht banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Tosaíocht Ghnáth" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Líon na n-iarrachtaí lódála Socraigh i gcás earráide." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Líon na n-iarrachtaí íoslódála i gcás earráide (ní thacaíonn uclient-fetch " +"leis seo)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Líon na n-iarrachtaí logála isteach theip ar an IP céanna sa logáil sula " +"ndéantar iad a bhlocáil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Díliostáil fothaí sonracha ón bpróiseas dídhúblála." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "An trácht amach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Feed Amach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Amach (paicéid)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Sáraigh an chumraíocht beatha réamhshocraithe agus cuir an beathú i bhfeidhm " +"ar an slabhra isteach agus amach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Sáraigh an chumraíocht beatha réamhshocraithe agus cuir an bheatha i " +"bhfeidhm ar an slabhra isteach amháin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Sáraigh an chumraíocht beatha réamhshocraithe agus cuir an beathú i bhfeidhm " +"ar an slabhra amach amháin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Sáraigh an chumraíocht fotha réamhshocraithe agus bain na teorainneacha " +"poirt/prótacal atá ann cheana féin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Sáraigh na roghanna íoslódála réamhchumraithe don fhóntas íoslódála " +"roghnaithe. Caithfidh an bratach aschuir, m.sh. '-o' le haghaidh curl nó '-" +"O' le haghaidh wget, a bheith mar an paraiméadar deireanach." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Forbhreathnú" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Ní déan ach an líon deireanach luaite iontrálacha logála le haghaidh " +"imeachtaí amhrasacha a pháirseáil. Chun an monatóir logála a dhíchumasú ar " +"chor ar bith socraigh é go '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Prótacal Port" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Log Próiseála" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Próifíl a úsáideann 'msmtp' le haghaidh Ríomhphoist fógra BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - ag freastal ar an Eoraip, an Meánoirthear agus Lár" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Ráta (in aghaidh an tsoicind) don teorainn loga NFT comhroinnte, curtha i " +"bhfeidhm go domhanda ar fud na rialacha logáilte go léir. Socraigh go '0' " +"chun teorainn ráta a dhíchumasú go hiomlán, m.sh. agus tú ag úsáid ulogd nó " +"láimhseálaithe loga spás úsáideora eile." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Faigh fógraí Ríomhphoist le gach reáchtáil BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Seoladh glacadóra le haghaidh Ríomhphoist fógra BanIP, teastaíonn an " +"fhaisnéis seo chun feidhmiúlacht R-phoist a chumasú." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Athnuaigh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Clár Réigiúnach Idirlín" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Sleachta rialta chun IPanna amhrasacha a bhrath i log an chórais. Úsáideann " +"sé an IP deireanach in aghaidh na líne; cuir 'first:' roimh théarma i gcás " +"formáidí foinse-ar-dtús, e.g. nginx." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Comhartha iargúlta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Imeacht logála cianda" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Eolaire Tuarascála" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Áiritheoirí Tuairiscithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "Cuir srian ar an rochtain idirlín ó/go líon beag IPanna slán." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Toradh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Riail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Bratacha Rith" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Faisnéis Rith" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "Tairseach Sync" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Tairseach Sync i bpacáistí in aghaidh an tsoicind chun ionsaithe WAN-DOS a " +"chosc. Chun an cosaint seo a dhíchumasú socraigh é go '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Sábháil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Sábháil agus Athlódáil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Sábháil & Atosaigh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Cuardaigh IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Tá an cuardach ar siúl, fan le do thoil..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Cuardaigh na Tacair a bhaineann le BANIP le haghaidh IP ar leith." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Chuaigh an t-am cuardaigh amach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Roghnaigh ceann amháin de na fóntais íoslódála réamh-chumraithe." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Roghnaigh an gléas (í) líonra WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Roghnaigh an comhéadan (í) líonra loighciúil WAN IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Roghnaigh an comhéadan (í) líonra loighciúil WAN IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Seoladh seoltóra le haghaidh Ríomhphoist fógra BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Socraigh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Socraigh Ábhar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Socraigh Ábhar..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Socraigh Trialacha Luchtaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Socraigh Polasaí" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Socraigh Tuairisc" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Socraigh Méid Scoilt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Socraigh sonraí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Socraigh tosaíocht an tslabhra NFT laistigh den tábla banIP, ciallaíonn " +"luachanna níos ísle tosaíocht níos airde." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Socraigh an polasaí NFT do Thacair a bhaineann le banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Socraigh an leibhéal syslog le haghaidh logáil NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Socruithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Taispeáin Ábhar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Taispeáin eilimintí Socraithe le buillí amháin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Scoilt ASN Set" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Tacar Tíre Scoilte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Scoilt luchtú Tacair sheachtraigh tar éis gach n ball chun RAM a shábháil, " +"díchumasaithe de réir réamhshocraithe." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Comhéadan Spriocdhírithe Tosaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Stádas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Stop" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Folíon" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" +"Achoimre ar thíortha bunaithe ar an gClárlann Idirlín Réigiúnach (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Eolas Córais" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Socruithe Tábla/Slabhra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Eolaire sprice do chomhaid earráide a bhaineann le BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Sprioc-eolaire do chomhaid tuarascála a bhaineann le BANIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Sprioceolaire le haghaidh cúltacaí beatha comhbhrúite." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Tá an liosta ceadanna ró-mhór, ní féidir na modhnuithe a shábháil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Tá an liosta blocála ró-mhór, ní féidir na modhnuithe a shábháil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Stóráiltear na ASNanna roghnaithe i dtacair ar leith." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Stóráiltear na Tíortha roghnaithe i dtacair ar leith." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Úsáidfear an tosaíocht roghnaithe le haghaidh próiseála cúlra BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"An t-aschur syslog, réamh-scagtha le haghaidh teachtaireachtaí a bhaineann " +"le: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Seo é an liosta ceadaithe banIP áitiúil a cheadóidh seoltaí MAC-, IP-nó " +"ainmneacha fearainn áirithe.
Tabhair faoi deara le do thoil: cuir díreach MAC/IPv4/IPv6 amháin leis seoladh nó ainm fearainn in " +"aghaidh na líne. Ceadaítear raonta i nodaireacht CIDR agus ceangail MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Seo é an blocliosta banIP áitiúil a chuirfidh cosc ar MAC-, seoltaí IP nó " +"ainmneacha fearainn áirithe.
Tabhair faoi deara le do thoil: cuir díreach MAC/IPv4/IPv6 amháin leis seoladh nó ainm fearainn in " +"aghaidh na líne. Ceadaítear raonta i nodaireacht CIDR agus ceangail MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Taispeánann an tuarascáil seo na staitisticí NFT Set is déanaí, brúigh an " +"cnaipe 'Athnuaigh' chun ceann nua a fháil. Is féidir leat freisin an t-ábhar " +"ar leith Seiteanna a thaispeáint, cuardach a dhéanamh ar IPanna amhrasach " +"agus ar deireadh, is féidir na IPanna seo a thaispeáint ar léarscáil freisin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Stampa ama" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Chun fógraí ríomhphoist a chumasú, socraigh an pacáiste 'msmtp' agus " +"sonraigh seoladh bailí glacadóra ríomhphoist." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Comhartha chun cumarsáid a dhéanamh leis an gcomhéadan cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Ábhar do Ríomhphoist fógra BanIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Moill Scorraitheoir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Tairseach UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Tairseach UDP i bpaicéid in aghaidh an tsoicind chun ionsaithe WAN-Dos a " +"chosc. Chun an cosaint seo a dhíchumasú socraigh é go '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "UrlV6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Ní féidir an fhaisnéis rith-ama banIP a pharsáil!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Ní féidir an comhad tíortha a pháirseáil!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Ní féidir an comhad beatha saincheaptha a pharsáil!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Ní féidir an comhad beatha réamhshocraithe a pharsáil!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Ní féidir an comhad rialacha a pháirseáil!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Ní féidir na modhnuithe a shábháil: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Uaslódáil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Theip ar uaslódáil an chomhaid beatha saincheaptha." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Logáil Dífhabhtú Eorbhéiseach" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Leis an eagarthóir seo is féidir leat do chomhad beatha saincheaptha áitiúil " +"a uaslódáil nó ceann tosaigh a líonadh suas (cóip 1:1 den leagan a sheoltar " +"leis an bpacáiste). Tá an comhad suite ag '/etc/banip/banip.custom.feeds'. " +"Ansin is féidir leat an comhad seo a chur in eagar, iontrálacha a scriosadh, " +"cinn nua a chur leis nó cúltaca áitiúil a dhéanamh. Chun dul ar ais chuig an " +"leagan cothabhálaí ná déan ach an comhad beatha saincheaptha a ghlanadh." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "foláireamh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "cuntas neamhbhailí réiltín" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "IPanna uathoibríoch leis an liosta ceadaithe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "IPanna uathoibríoch a chur leis an liosta bloc" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "BaniP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "paicéid bcp38 blocáilte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "pacáistí icmp-flood blocáilte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "bac ar phacéid ct neamhbhailí" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "pacáistí tcp neamhbhailí blocáilte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "pacáistí syn-tuile blocáilte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "pacáistí udp-tuile blocáilte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "dífhabhtú" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "titim" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "Theip ar logáil isteach dropbear" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "emerg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "earráid" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "eolas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "liosta ceadanna áitiúil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "blocliosta áitiúil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "cuimhne" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "fógra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "doiciméadú ar líne" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "Earráid openvpn TLS" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "feidhmíocht" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "diúltú" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "nasc dúnta sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "Theip ar logáil isteach sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "rabhadh" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "Sloinn rialta chun IPanna amhrasacha a bhrath i loga an chórais." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Sáraigh na roghanna íoslódála réamhchumraithe don áirgiúlacht íoslódála " +#~ "roghnaithe." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Cumasaigh cuntar nft do gach eilimint Set." + +#~ msgid "Set Element Counter" +#~ msgstr "Socraigh Eilimint Áiritheoir" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Socraigh tosaíocht an slabhra nft laistigh den tábla BanIP, ciallaíonn " +#~ "luachanna níos ísle tosaíocht níos airde." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Socraigh an beartas nft do Sraith a bhaineann le BanIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx IP amhrasach" + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Invalid expiry format" +#~ msgstr "Formáid éaga neamhbhailí" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Ní thacaítear le formáid an phrótacail/URL" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Chun fógraí ríomhphoist a chumasú, cuir an pacáiste 'msmtp' ar bun agus " +#~ "sonraigh seoladh glacadóra ríomhphoist vaild." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Ní féidir an fhaisnéis rith-ama a pharsáil!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Cumraíocht an phacáiste banIP chun IPanna isteach agus amach a thoirmeasc " +#~ "trí Thacair nftables ainmnithe. Le haghaidh tuilleadh eolais seiceáil an " +#~ "doiciméid ar líne" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", fotheaghrán" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", comhtháite" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "DO" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Rule4" + +#~ msgid "Rulev6" +#~ msgstr "Rule6" + +#~ msgid "Version" +#~ msgstr "Leagan" + +#~ msgid "logread not found on system." +#~ msgstr "Níor aimsíodh an léamh loga ar an gcóras." + +#~ msgid "banIP Log" +#~ msgstr "Logáil IP toirmiscthe" + +#~ msgid "banIP firewall Log" +#~ msgstr "Logáil balla dóiteáin banIP" + +#~ msgid "Elements" +#~ msgstr "Eilimintí" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "Níl Léarscáil GeoIP cumasaithe!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Ní féidir an comhad tuarascála a pháirseáil!" diff --git a/luci-app-secbox/po/he/secbox.po b/luci-app-secbox/po/he/secbox.po new file mode 100644 index 000000000..60f3f95e5 --- /dev/null +++ b/luci-app-secbox/po/he/secbox.po @@ -0,0 +1,1325 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-01-20 14:25+0000\n" +"Last-Translator: Yaron Shahrabani \n" +"Language-Team: Hebrew \n" +"Language: he\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " +"n % 10 == 0) ? 2 : 3));\n" +"X-Generator: Weblate 5.16-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "תיקיית גיבוי" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "תיקיית בסיס" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "תיאור" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "הגדרות כלליות" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "מצב" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/hi/secbox.po b/luci-app-secbox/po/hi/secbox.po new file mode 100644 index 000000000..034a508bd --- /dev/null +++ b/luci-app-secbox/po/hi/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-07-06 11:32+0000\n" +"Last-Translator: Sathvic \n" +"Language-Team: Hindi \n" +"Language: hi\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.7-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "उन्नत सेटिंग्स" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "रद्द करें" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "स्थिति" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/hu/secbox.po b/luci-app-secbox/po/hu/secbox.po new file mode 100644 index 000000000..f2b0ff77f --- /dev/null +++ b/luci-app-secbox/po/hu/secbox.po @@ -0,0 +1,1327 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-03-08 17:32+0000\n" +"Last-Translator: hmzs \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.17-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "AS számok" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktív eszközök" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"További aktiválókésleltetés másodpercben, mielőtt a banIP feldolgozása " +"elkezdődik." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Haladó beállítások" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatikus észlelés" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Biztonsági mentés könyvtára" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Mégse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Országok" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Leírás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Letöltési segédprogram" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "E-mail értesítés" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-Mail profil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-Mail küldő cím" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +#, fuzzy +msgid "E-Mail Topic" +msgstr "E-Mail téma" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Engedélyezve" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Általános beállítások" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 támogatás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 támogatás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Információ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Utolsó futás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Legkisebb prioritás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Kisebb prioritás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Közepes prioritás (alapértelmezett)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Áttekintés" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Frissítés" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Eredmény" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Mentés" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Beállítások" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Aktiváló késleltetése" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Részletes hibakeresési naplózás" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/it/secbox.po b/luci-app-secbox/po/it/secbox.po new file mode 100644 index 000000000..026b74f71 --- /dev/null +++ b/luci-app-secbox/po/it/secbox.po @@ -0,0 +1,1429 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-05-26 15:21+0000\n" +"Last-Translator: Stefano Mora \n" +"Language-Team: Italian \n" +"Language: it\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.6.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Seleziona (opzionale) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Imposta selezione --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- default --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - serve l'Africa e la regione dell'Oceano Indiano" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - serve la regione Asia Pacifica" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - serve il Canada e gli Stati Uniti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Dispositivi attivi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Feed attivi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Uplink attivi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Impostazioni Avanzate" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Consenti protocollo/porte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Consenti l'inoltro delle VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Consentire lista URL di feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Solo Lista consentiti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Consenti sempre un protocollo (tcp/udp) con determinate porte o intervalli " +"di porte nelle catene WAN-Input e WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Permetti sempre alcuni inoltri VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Blocca sempre alcuni inoltri VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Consenti automaticamente Uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Auto-Lista consentiti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Blocco automatico sottoreti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Auto-Lista bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Rilevamento automatico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Aggiungi automaticamente intere sottoreti alla lista bloccati in base a " +"un'ulteriore richiesta RDAP con l'IP sospetto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Aggiungi automaticamente i domini risolti e gli IP sospetti alla lista " +"bloccati locale di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Aggiungi automaticamente i domini risolti e gli IP di uplink alla lista " +"consentiti locale di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Cartella del backup" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Cartella di base" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Cartella di lavoro di base durante l'elaborazione di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Blocco dell'inoltro delle VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Feed della lista di blocco" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Scadenza set Lista bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Core CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Annulla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Priorità della catena" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Per applicare le modifiche in questa scheda serve riavviare il servizio " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Paesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Editor di feed personalizzati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Deduplica gli indirizzi IP su tutti i set attivi e riordina la lista " +"bloccati locale." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Deduplica gli IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Descrizione" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Rileva automaticamente i dispositivi di rete, le interfacce, le sottoreti, i " +"protocolli e le utilità rilevanti." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Disattiva" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Non controllare i certificati SSL del server durante il download." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Download non sicuro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parametri di download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Tentativi di download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Utilità di download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notifica e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Profilo e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Indirizzo e-mail destinatario" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Indirizzo e-mail mittente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Impostazioni e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Oggetto e-mail" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Modifica Lista consentiti" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Modifica Lista bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Numero elementi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Campo vuoto non consentito" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Abilita il log Remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Attiva il servizio banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Abilita l'interfaccia cgi a ricevere eventi di log remoti." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Attiva log di debug esteso in caso di errori di elaborazione." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Abilita" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Attiva il supporto IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Attiva il supporto IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Tempo di scadenza per i membri del set Lista bloccati aggiunti " +"automaticamente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Feed di Lista consentiti esterna" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Feed di Lista bloccati esterna" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Nome feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Selezione feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Impostazioni Feed/Set" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Registro del firewall" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Flag" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Flag non supportato" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Impostazioni Generali" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Consenti l'accesso all'app banIP di LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Priorità alta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Priorità massima" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-Threshold" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Ricerca IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Ricerca IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Interfacce di rete IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Supporto IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Interfacce di rete IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Supporto IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Aumenta il numero massimo di file aperti, ad es. per gestire la quantità di " +"file divisi temporanei durante il caricamento dei set." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informazioni" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Caratteri non validi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Valori di input non validi, impossibile salvare le modifiche." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - serve la regione dell'America Latina e dei Caraibi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Ultimo avvio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Priorità minima" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Priorità bassa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Limita la funzione auto-consentiti di uplink." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Elenco delle interfacce di rete disponibili per innescare l'avvio di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Elenca gli elementi di un set specifico relativo a banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Impostazioni feed locale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Posizione per l'analisi del file di log, ad esempio tramite syslog-ng, per " +"disattivare l'analisi standard tramite logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Numero log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Limite di log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Registra il prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Impostazioni log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Termini del log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Posizione del file di log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Max file aperti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Informazioni NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Livello log NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Dispositivi di rete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Livello priorità (nice)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Priorità normale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Numero di tentativi di download in caso di errore (non supportato da uclient-" +"fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Numero di tentativi di accesso falliti dello stesso IP nel log prima del " +"blocco." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Riepilogo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analizza solo l'ultimo numero dichiarato di voci del log per eventi " +"sospetti. Per disattivare del tutto il monitor del log, impostalo a '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Log di elaborazione" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Profilo usato da 'msmtp' per le notifiche e-mail di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - serve l' Europa, Medio Oriente e Asia Centrale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Ricevi notifiche e-mail ad ogni esecuzione di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Indirizzo del destinatario per le e-mail di notifica banIP, queste " +"informazioni sono necessarie per attivare la funzionalità e-mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Aggiorna" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Registro di Internet regionale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Token Remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Cartella del report" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "Limita l'accesso a Internet da/a un numero limitato di IP sicuri." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Risultato" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Avvia flags" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Avvia informazioni" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN-Threshold" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Salva" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Cerca i set relativi a banIP per un IP specifico." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Seleziona una delle utilità di download preconfigurate." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Seleziona il/i dispositivo/i di rete WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Seleziona l'/le interfaccia/e di rete WAN IPv4 logica/che." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Seleziona l'/le interfaccia/e di rete WAN IPv6 logica/che." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Indirizzo del mittente per le e-mail di notifica di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Imposta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Criteri di impostazione" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Imposta resoconti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Imposta dim. divisione" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Imposta dettagli" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Imposta il livello syslog per la registrazione NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Impostazioni" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interfaccia trigger di avvio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Stato" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Ferma" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Sottorete" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Impostazioni della Tabella/Chain" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Cartella di destinazione per i file di report relativi a banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Cartella di destinazione per i backup dei feed compressi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "La lista consentiti è troppo grande, impossibile salvare le modifiche." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "La lista bloccati è troppo grande, impossibile salvare le modifiche." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"La priorità selezionata verrà usata per l'elaborazione in secondo piano di " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Questa è la Lista consentiti locale di banIP che consentirà determinati " +"indirizzi MAC, indirizzi IP o nomi di dominio.
Nota: " +"aggiungi esattamente un solo indirizzo MAC/IPv4/IPv6 o nome di dominio per " +"riga. Sono consentiti gli intervalli nella notazione CIDR e le associazioni " +"MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Questa è la Lista bloccati locale di banIP che impedirà determinati " +"indirizzi MAC, indirizzi IP o nomi di dominio.
Nota: " +"aggiungi solo esattamente un indirizzo MAC/IPv4/IPv6 o nome di dominio per " +"riga. Sono consentiti gli intervalli nella notazione CIDR e le associazioni " +"MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Marca temporale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token per comunicare con l'interfaccia cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Oggetto per le e-mail di notifica di banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Ritardo innesco" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP-Threshold" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Impossibile analizzare il file dei paesi!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Impossibile analizzare il file del feed personalizzato!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Impossibile analizzare il file di feed predefinito!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Impossibile analizzare il file del set di regole!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Impossibile salvare le modifiche: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Caricamento del file dei feed personalizzati fallito." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Registro di debug dettagliato" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "allarme" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "Indirizzi IP aggiunti automaticamente alla whitelist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "Indirizzi IP aggiunti automaticamente alla blacklist" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "pacchetti icmp-flood bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "pacchetti ct non validi bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "pacchetti tcp non validi bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "pacchetti syn-flood bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "pacchetti udp-flood bloccati" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "debug" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "scarta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "emerg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "err" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lista consentiti locale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lista bloccati locale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "memoria" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "notifica" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "prestazioni" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "rifiuta (reject)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "avviso" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Sovrascrivi le opzioni di download preconfigurate per l'utilità di " +#~ "download selezionata." + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Imposta la priorità della catena nft all'interno della tabella banIP; " +#~ "valori inferiori indicano priorità più elevate." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Imposta il criterio nft per iset relativi a banIP." + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Protocollo/Formato URL non supportato" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Per attivare le notifiche e-mail, configura il pacchetto 'msmtp' e " +#~ "specifica un indirizzo di destinatario e-mail valido." + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Configurazione del divieto Pacchetto IP per vietare IP in entrata e in " +#~ "uscita tramite nftables Sets. Per ulteriori informazioni, consultare la " +#~ "documentazione online" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Versione" + +#~ msgid "Elements" +#~ msgstr "Elementi" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Impossibile analizzare il file di report!" diff --git a/luci-app-secbox/po/ja/secbox.po b/luci-app-secbox/po/ja/secbox.po new file mode 100644 index 000000000..65f0cc491 --- /dev/null +++ b/luci-app-secbox/po/ja/secbox.po @@ -0,0 +1,1351 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-29 17:04+0000\n" +"Last-Translator: ryosuke iwasaki \n" +"Language-Team: Japanese \n" +"Language: ja\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- 選択 --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- 既定 --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC (アフリカおよびインド洋)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC (アジア太平洋)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN (カナダおよびアメリカ)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN 選択" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "アクティブなデバイス" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "アクティブなフィード" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "アクティブなアップリンク" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "詳細設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "自動検出" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "バックアップ先 ディレクトリ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPUコア" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "キャンセル" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "チェーン" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "クリア" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "カウント" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "国" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "説明" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "無効" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "ダウンロード" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "ダウンロードのパラメータ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "ダウンロードユーティリティ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Eメール通知" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Eメールプロファイル" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Eメール受信アドレス" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Eメール送信者アドレス" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "メール設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "メール トピック" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "許可リストを編集" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "拒否リストを編集" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "空白は許可されていません" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "banIP サービスを有効にする。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "有効" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "一般設定" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "高い優先度" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "最高の優先度" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 サポート" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 サポート" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "情報" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "最終実行" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "最低の優先度" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "低い優先度" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "ログ制限" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "通常の優先度 (デフォルト)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "概要" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "リフレッシュ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "レポート ディレクトリ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "結果" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "実行フラグ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "実行情報" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "保存" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "保存して再読み込み" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "保存して再実行" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "起動時トリガーインターフェース" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "状態" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "停止" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "サブネット" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "システム情報" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "タイムスタンプ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "トリガ遅延" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "アップロード" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "詳細なデバッグ ログ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "情報" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" diff --git a/luci-app-secbox/po/ko/secbox.po b/luci-app-secbox/po/ko/secbox.po new file mode 100644 index 000000000..703ba60d6 --- /dev/null +++ b/luci-app-secbox/po/ko/secbox.po @@ -0,0 +1,1338 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-01 20:06+0000\n" +"Last-Translator: Hyeonjeong Lee \n" +"Language-Team: Korean \n" +"Language: ko\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 2026.7.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- 선택하세요 (선택 사항) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- 기본값 --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "<공백><넷마스크>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN 선택" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "활성 장치" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "활성 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "활성 업링크" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "AdGuard Home 로그인 오류" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "고급 설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "허용 목록 피드 URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "자동 허용 목록" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "자동 차단 목록" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "자동 감지" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "백업 디렉터리" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "기준 디렉터리" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "차단 목록 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU 코어" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "취소" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "체인" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "체인 우선순위" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "이 탭의 변경 사항을 적용하려면 banIP 서비스를 재시작해야 합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "초기화" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "수" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "국가" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "국가 선택" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "사용자 정의 피드 편집기" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "설명" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "비활성화" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "다운로드 시 SSL 서버 인증서를 검증하지 않습니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "다운로드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "안전하지 않은 다운로드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "다운로드 매개변수" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "다운로드 재시도" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "다운로드 유틸리티" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "이메일 알림" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "이메일 프로필" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "이메일 수신자 주소" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "이메일 발신자 주소" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "이메일 설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "이메일 제목" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "허용 목록 편집" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "차단 목록 편집" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "필수 입력 항목" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "BCP38 활성화" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "원격 로그 기록 사용" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "banIP 서비스를 활성화합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "처리 오류 발생 시 상세 디버그 로그 기록을 활성화합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "활성화" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "IPv4 지원을 활성화합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "IPv6 지원을 활성화합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "오류 디렉터리" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "외부 허용 목록 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "외부 차단 목록 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "피드 이름" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "피드 선택" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "피드/세트 설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "채우기" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "방화벽 로그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "플래그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "지원되지 않는 플래그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "일반 설정" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "LuCI 앱 banIP에 접근 권한 부여" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "높은 우선순위" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "최고 우선순위" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP 검색" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP 검색..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4 네트워크 인터페이스" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 지원" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6 네트워크 인터페이스" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 지원" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "인바운드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "인바운드 및 아웃바운드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "인바운드 및 아웃바운드 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "인바운드 차단 정책" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "인바운드 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "정보" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "허용되지 않는 문자" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "잘못된 입력 값입니다. 변경 사항을 저장할 수 없습니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "최근 실행" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "최저 우선순위" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "낮은 우선순위" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "로컬 피드 설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "인바운드 로그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "로그 제한" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "아웃바운드 로그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "사전 라우팅 로그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "로그 설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "LAN-Forward 체인의 의심스러운 패킷을 기록합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Prerouting 체인의 의심스러운 패킷을 기록합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "WAN-Input 및 WAN-Forward 체인의 의심스러운 패킷을 기록합니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "LuCI 로그인 실패" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "지도 초기화" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT 정보" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "NFT 로그 수준" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "네트워크 장치" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nice 레벨" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "아직 %s 관련 로그 없음!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "GeoIP 지도 데이터가 없습니다!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "보통 우선순위" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "오류 발생 시 다운로드 시도 횟수입니다. (uclient-fetch 미지원)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "아웃바운드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "아웃바운드 피드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"선택한 다운로드 유틸리티의 사전 설정된 다운로드 옵션을 변경합니다. 출력 플래" +"그(예: curl의 경우 '-o', wget의 경우 '-O')는 반드시 마지막 매개변수여야 합니" +"다." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "개요" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "새로 고침" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "원격 토큰" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "보고서 디렉터리" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "결과" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "규칙" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "실행 플래그" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "실행 정보" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "저장" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "저장 및 다시 불러오기" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "저장 및 재시작" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "IP 검색" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "사전 정의된 다운로드 유틸리티 중 하나를 선택하세요." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "WAN 네트워크 장치를 선택하세요." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "논리 WAN IPv4 네트워크 인터페이스를 선택하세요." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "논리 WAN IPv6 네트워크 인터페이스를 선택하세요." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "세트 정책" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"banIP 테이블 내의 NFT 체인 우선순위를 설정합니다. 값이 낮을수록 우선순위가 높" +"습니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "시작 트리거 인터페이스" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "상태" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "중지" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "서브넷" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "시스템 정보" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "테이블/체인 설정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "허용 목록이 너무 커서 변경 사항을 저장할 수 없습니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "차단 목록이 너무 커서 변경 사항을 저장할 수 없습니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "syslog 출력 (필터링 대상 메시지: %s)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "시작 지연 시간" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "IPv4 피드 URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "IPv6 피드 URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "사용자 정의 피드 파일을 분석할 수 없습니다!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "기본 피드 파일을 분석할 수 없습니다!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "변경 사항을 저장할 수 없습니다: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "업로드" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "사용자 정의 피드 파일 업로드에 실패했습니다." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "상세 디버그 로그 기록" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "경보(alert)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "Asterisk 잘못된 계정" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "치명적(crit)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "디버그(debug)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "드롭" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "Dropbear 로그인 실패" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "긴급(emerg)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "오류(err)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "정보(info)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "로컬 허용 목록" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "로컬 차단 목록" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "알림(notice)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "온라인 문서" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "OpenVPN TLS 오류" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "성능(performance)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "거부" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "sshd 로그인 실패" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "경고(warn)" + +#~ msgid "" +#~ msgstr "" + +#~ msgid " " +#~ msgstr " <서브넷 마스크>" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "지원되지 않는 프로토콜 또는 URL 형식" diff --git a/luci-app-secbox/po/lo/secbox.po b/luci-app-secbox/po/lo/secbox.po new file mode 100644 index 000000000..be606bf64 --- /dev/null +++ b/luci-app-secbox/po/lo/secbox.po @@ -0,0 +1,1375 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-24 15:20+0000\n" +"Last-Translator: BoneNI \n" +"Language-Team: Lao \n" +"Language: lo\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- ກະລຸນາເລືອກ (ທາງເລືອກ) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- ເລືອກຊຸດ --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- ຄ່າເລີ່ມຕົ້ນ --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "<ທີ່ຢູ່ IP>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "<ທີ່ຢູ່ IP><ຕົວແຍກ CSV>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "<ທີ່ຢູ່ IP><ຊ່ອງຫວ່າງ>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "<ຮູບແບບ Suricata>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - ໃຫ້ບໍລິການພາກພື້ນອາຟຣິກາ ແລະ ມະຫາສະໝຸດອິນເດຍ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - ໃຫ້ບໍລິການພາກພື້ນອາຊີປາຊີຟິກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - ໃຫ້ບໍລິການປະເທດການາດາ ແລະ ສະຫະລັດອາເມຣິກາ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ການເລືອກ ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "ອຸປະກອນທີ່ເຮັດວຽກຢູ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "ຟີດທີ່ເປີດໃຊ້ງານ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Uplink ທີ່ເຮັດວຽກຢູ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "ຂໍ້ຜິດພາດໃນການເຂົ້າສູ່ລະບົບ AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "ເພີ່ມເວລາໜ່ວງໃນການກະຕຸ້ນ (ເປັນວິນາທີ) ກ່ອນທີ່ banIP ຈະເລີ່ມປະມວນຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "ການຕັ້ງຄ່າຂັ້ນສູງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "ອະນຸຍາດໂປຣໂຕຄອນ/ພອດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "ອະນຸຍາດການສົ່ງຕໍ່ VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URLs ຂອງຟີດລາຍການອະນຸຍາດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "ສະເພາະລາຍການອະນຸຍາດເທົ່ານັ້ນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "ບັນທຶກການດັດແກ້ລາຍການອະນຸຍາດແລ້ວ, ໃຫ້ໂຫຼດ banIP ໃໝ່ເພື່ອໃຫ້ການປ່ຽນແປງມີຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"ອະນຸຍາດໂປຣໂຕຄອນ (tcp/udp) ທີ່ມີພອດ ຫຼື ຊ່ວງພອດທີ່ກຳນົດໄວ້ໃນ Chain WAN-Input ແລະ WAN-" +"Forward ສະເໝີ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "ອະນຸຍາດການສົ່ງຕໍ່ VLAN ບາງຢ່າງສະເໝີ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "ບລັອກການສົ່ງຕໍ່ VLAN ບາງຢ່າງສະເໝີ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "ອະນຸຍາດ Uplink ໂດຍອັດຕະໂນມັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "ລາຍການອະນຸຍາດອັດຕະໂນມັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "ບລັອກ Subnet ໂດຍອັດຕະໂນມັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "ລາຍການບລັອກອັດຕະໂນມັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "ກວດຫາອັດຕະໂນມັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"ເພີ່ມ Subnet ທັງໝົດເຂົ້າໃນຊຸດລາຍການບລັອກໂດຍອັດຕະໂນມັດ ໂດຍອີງໃສ່ການຮ້ອງຂໍ RDAP ເພີ່ມເຕີມກັບ IP " +"ທີ່ໜ້າສົງໄສ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "ເພີ່ມໂດເມນທີ່ແກ້ໄຂໄດ້ ແລະ IP ທີ່ໜ້າສົງໄສເຂົ້າໃນລາຍການບລັອກ banIP ທ້ອງຖິ່ນໂດຍອັດຕະໂນມັດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"ເພີ່ມໂດເມນທີ່ແກ້ໄຂໄດ້ ແລະ IP Uplink ເຂົ້າໃນລາຍການອະນຸຍາດ banIP ທ້ອງຖິ່ນໂດຍອັດຕະໂນມັດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "ໄດເຣັກທໍຣີສຳຮອງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "ໄດເຣັກທໍຣີຫຼັກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "ໄດເຣັກທໍຣີເຮັດວຽກຫຼັກໃນຂະນະທີ່ banIP ປະມວນຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "ບລັອກການສົ່ງຕໍ່ VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "ບລັອກແພັກເກັດທີ່ມີທີ່ຢູ່ IP ຕົ້ນທາງປອມໃນທຸກ Chains ທີ່ຮອງຮັບ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "ຟີດລາຍການກີດກັນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "ໝົດອາຍຸຂອງຊຸດລາຍການບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "ບັນທຶກການດັດແກ້ລາຍການບລັອກແລ້ວ, ໃຫ້ໂຫຼດ banIP ໃໝ່ເພື່ອໃຫ້ການປ່ຽນແປງມີຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "ຂະໜາດ Burst ເປັນແພັກເກັດສຳລັບການຈຳກັດການບັນທຶກ NFT ທີ່ໃຊ້ຮ່ວມກັນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "ແກນຊີພີຢູ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "ຍົກເລີກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Chain" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "ລຳດັບຄວາມສຳຄັນຂອງ Chain" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "ການປ່ຽນແປງໃນແຖບນີ້ຕ້ອງມີການໂຫຼດບໍລິການ banIP ໃໝ່ເພື່ອໃຫ້ມີຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "ການປ່ຽນແປງໃນແຖບນີ້ຕ້ອງມີການຣີສະຕາດບໍລິການ banIP ເພື່ອໃຫ້ມີຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "ລ້າງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "ການເກັບກຳທີ່ຢູ່ IP ໂດຍອີງໃສ່ເລກລະບົບອັດຕະໂນມັດ (ASNs)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"ການຕັ້ງຄ່າແພັກເກັດ banIP ເພື່ອບລັອກ IP ຂາເຂົ້າ ແລະ ຂາອອກຜ່ານ nftables Sets. " +"ສຳລັບຂໍ້ມູນເພີ່ມເຕີມ ກະລຸນາກວດສອບ %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "ຈຳນວນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "ປະເທດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "ການເລືອກປະເທດ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "ແກ້ໄຂຟີດແບບກຳນົດເອງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "ລຶບທີ່ຢູ່ IP ທີ່ຊ້ຳກັນໃນທຸກໆຊຸດທີ່ເປີດໃຊ້ງານ ແລະ ຈັດລະບຽບລາຍການບລັອກທ້ອງຖິ່ນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "ລຶບ IP ທີ່ຊ້ຳກັນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "ຄຳອະທິບາຍ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"ກວດຫາອຸປະກອນເຄືອຂ່າຍ, ອິນເຕີເຟດ, Subnets, ໂປຣໂຕຄອນ ແລະ ເຄື່ອງມືທີ່ກ່ຽວຂ້ອງໂດຍອັດຕະໂນມັດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "ປິດໃຊ້ງານ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "ບໍ່ຕ້ອງກວດສອບໃບຢັ້ງຢືນ SSL ຂອງເຄື່ອງແມ່ຂ່າຍໃນລະຫວ່າງການດາວໂຫຼດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "ດາວໂຫຼດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "ດາວໂຫຼດແບບບໍ່ປອດໄພ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "ພາຣາມິເຕີການດາວໂຫຼດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "ຈຳນວນຄັ້ງການດາວໂຫຼດຄືນໃໝ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "ເຄື່ອງມືດາວໂຫຼດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "ວາງ (Drop) ແພັກເກັດຢ່າງງຽບໆ ຫຼື ປະຕິເສດການຈະລາຈອນຂາເຂົ້າຢ່າງຕັ້ງໜ້າ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "ການແຈ້ງເຕືອນທາງອີເມວ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "ໂປຣໄຟລ໌ອີເມວ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "ທີ່ຢູື່ອີເມວຜູ້ຮັບ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "ທີ່ຢູື່ອີເມວຜູ້ສົ່ງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "ການຕັ້ງຄ່າອີເມວ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "ຫົວຂໍ້ອີເມວ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "ແກ້ໄຂລາຍການອະນຸຍາດ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "ແກ້ໄຂລາຍການກີດກັນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "ຈຳນວນອົງປະກອບ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "ອົງປະກອບ (ສູງສຸດ 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "ຫ້າມປ່ອຍຫວ່າງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "ເປີດໃຊ້ງານ BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "ເປີດໃຊ້ງານແຜນທີ່ GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"ເປີດໃຊ້ງານ NFT counters ສຳລັບອົງປະກອບຊຸດ ແລະ ກົດຂອງ Chain. ຈຳເປັນສຳລັບແຜນທີ່ GeoIP ແລະ " +"ສະຖິຕິແພັກເກັດໃນການລາຍງານຊຸດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "ເປີດໃຊ້ງານການບັນທຶກຈາກທາງໄກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"ເປີດໃຊ້ງານແຜນທີ່ GeoIP ທີ່ມີອົງປະກອບຊຸດທີ່ໜ້າສົງໄສ. ສິ່ງນີ້ຕ້ອງການການຮ້ອງຂໍພາຍນອກເພື່ອຮັບແຜນທີ່ ແລະ " +"ຂໍ້ມູນທາງພູມິສາດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "ເປີດໃຊ້ງານບໍລິການ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "ເປີດໃຊ້ງານອິນເຕີເຟດ cgi ເພື່ອຮັບເຫດການບັນທຶກຈາກທາງໄກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "ເປີດໃຊ້ງານການບັນທຶກດີບັກລະອຽດໃນກໍລະນີເກີດຂໍ້ຜິດພາດໃນການປະມວນຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "ເປີດໃຊ້ງານ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "ເປີດໃຊ້ງານການຮອງຮັບ IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "ເປີດໃຊ້ງານການຮອງຮັບ IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "ໄດເຣັກທໍຣີເກັບຂໍ້ຜິດພາດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "ເວລາໝົດອາຍຸສຳລັບສະມາຊິກຊຸດລາຍການບລັອກທີ່ເພີ່ມອັດຕະໂນມັດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "ຟີດລາຍການອະນຸຍາດພາຍນອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "ຟີດລາຍການກີດກັນພາຍນອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "ລົ້ມເຫຼວໃນການສ້າງລາຍງານ banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "ຟີດສຳເລັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "ຣີເຊັດທຸງຟີດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "ຊື່ຟີດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "ການເລືອກຟີດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "ການຕັ້ງຄ່າຟີດ/ຊຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "ຕື່ມ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "ບັນທຶກໄຟວໍ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "ທຸງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "ບໍ່ຮອງຮັບທຸງນີ້" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "ການຕັ້ງຄ່າທົ່ວໄປ" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "ໃຫ້ສິດເຂົ້າເຖິງແອັບ banIP ໃນ LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "ຄວາມສຳຄັນສູງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "ຄວາມສຳຄັນສູງສຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ຂອບເຂດ ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ຂອບເຂດ ICMP ເປັນແພັກເກັດຕໍ່ວິນາທີເພື່ອປ້ອງກັນການໂຈມຕີ WAN-DoS. ຖ້າຕ້ອງການປິດການປ້ອງກັນນີ້ " +"ໃຫ້ຕັ້ງເປັນ '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "ຄົ້ນຫາ IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "ກຳລັງຄົ້ນຫາ IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "ອິນເຕີເຟດເຄືອຂ່າຍ IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "ການຮອງຮັບ IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "ອິນເຕີເຟດເຄືອຂ່າຍ IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "ການຮອງຮັບ IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "ຂາເຂົ້າ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "ຂາເຂົ້າ & ຂາອອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "ຟີດຂາເຂົ້າ & ຂາອອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "ນະໂຍບາຍການບລັອກຂາເຂົ້າ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "ຟີດຂາເຂົ້າ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "ຂາເຂົ້າ (ແພັກເກັດ)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "ເພີ່ມຈຳນວນໄຟລ໌ທີ່ເປີດສູງສຸດ, ເຊັ່ນ: ເພື່ອຈັດການກັບຈຳນວນໄຟລ໌ແຍກຊົ່ວຄາວໃນຂະນະທີ່ກຳລັງໂຫຼດຊຸດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "ຂໍ້ມູນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "ຮູບແບບ URL ບໍ່ຖືກຕ້ອງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "ຕົວອັກສອນບໍ່ຖືກຕ້ອງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "ຮູບແບບການໝົດອາຍຸບໍ່ຖືກຕ້ອງ, ເຊັ່ນ: 5m, 2h, 1d ຫຼື 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "ຄ່າທີ່ປ້ອນບໍ່ຖືກຕ້ອງ, ບໍ່ສາມາດບັນທຶກການແກ້ໄຂໄດ້." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - ໃຫ້ບໍລິການພາກພື້ນອາເມລິກາລາຕິນ ແລະ ຄາຣິບຽນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "ການເຮັດວຽກຄັ້ງຫຼ້າສຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "ຄວາມສຳຄັນຕ່ຳສຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "ຄວາມສຳຄັນຕ່ຳ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "ຈຳກັດແກນ CPU ທີ່ banIP ໃຊ້ເພື່ອປະຢັດ RAM, ກວດຫາອັດຕະໂນມັດໂດຍຄ່າເລີ່ມຕົ້ນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "ຈຳກັດຟັງຊັນການອະນຸຍາດ Uplink ອັດຕະໂນມັດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "ລາຍການອິນເຕີເຟດເຄືອຂ່າຍທີ່ມີຢູ່ເພື່ອສັ່ງໃຫ້ banIP ເລີ່ມເຮັດວຽກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "ລາຍການອົງປະກອບຂອງຊຸດທີ່ກ່ຽວຂ້ອງກັບ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "ການຕັ້ງຄ່າຟີດທ້ອງຖິ່ນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"ສະຖານທີ່ສຳລັບການວິເຄາະໄຟລ໌ບັນທຶກ, ເຊັ່ນ: ຜ່ານ syslog-ng, ເພື່ອປິດການວິເຄາະມາດຕະຖານຜ່ານ " +"logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "ຈຳກັດການບັນທຶກແບບ Burst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "ຈຳນວນການບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "ບັນທຶກຂາເຂົ້າ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "ຈຳກັດການບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "ບັນທຶກຂາອອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "ບັນທຶກ Prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "ຈຳກັດອັດຕາການບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "ການຕັ້ງຄ່າບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "ເງື່ອນໄຂການບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "ບັນທຶກແພັກເກັດທີ່ໜ້າສົງໄສໃນ Chain LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "ບັນທຶກແພັກເກັດທີ່ໜ້າສົງໄສໃນ Chain Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "ບັນທຶກແພັກເກັດທີ່ໜ້າສົງໄສໃນ Chain WAN-Input ແລະ WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "ສະຖານທີ່ໄຟລ໌ບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "ການເຂົ້າສູ່ລະບົບ LuCI ລົ້ມເຫຼວ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "ຣີເຊັດແຜນທີ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "ແຜນທີ່..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "ໄຟລ໌ທີ່ເປີດໄດ້ສູງສຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "ຂໍ້ມູນ NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "ລະດັບການບັນທຶກ NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "ອຸປະກອນເຄືອຂ່າຍ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "ຂໍ້ຜິດພາດຂອງເຄືອຂ່າຍ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "ລະດັບ Nice" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "ຍັງບໍ່ມີບັນທຶກເຫດການທີ່ກ່ຽວຂ້ອງກັບ %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "ບໍ່ມີຂໍ້ມູນແຜນທີ່ GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "ບໍ່ພົບການຕັ້ງຄ່າ banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "ຄວາມສຳຄັນປົກກະຕິ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "ຈຳນວນຄັ້ງໃນການພະຍາຍາມໂຫຼດຊຸດ ໃນກໍລະນີເກີດຂໍ້ຜິດພາດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"ຈຳນວນຄັ້ງຂອງຄວາມພະຍາຍາມດາວໂຫຼດ ໃນກໍລະນີທີ່ເກີດຂໍ້ຜິດພາດ (ບໍ່ຮອງຮັບໂດຍ uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "ຈຳນວນຄັ້ງຂອງການເຂົ້າສູ່ລະບົບລົ້ມເຫຼວຈາກ IP ດຽວກັນໃນບັນທຶກ ກ່ອນທີ່ຈະຖືກບລັອກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "ຍົກເວັ້ນຟີດສະເພາະຈາກຂະບວນການລົບຂໍ້ມູນຊ້ຳ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "ຂາອອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "ຟີດຂາອອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "ຂາອອກ (ແພັກເກັດ)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "ຂຽນທັບການຕັ້ງຄ່າຟີດເລີ່ມຕົ້ນ ແລະ ນຳໃຊ້ຟີດກັບ Chain ຂາເຂົ້າ ແລະ ຂາອອກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "ຂຽນທັບການຕັ້ງຄ່າຟີດເລີ່ມຕົ້ນ ແລະ ນຳໃຊ້ຟີດກັບ Chain ຂາເຂົ້າເທົ່ານັ້ນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "ຂຽນທັບການຕັ້ງຄ່າຟີດເລີ່ມຕົ້ນ ແລະ ນຳໃຊ້ຟີດກັບ Chain ຂາອອກເທົ່ານັ້ນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "ຂຽນທັບການຕັ້ງຄ່າຟີດເລີ່ມຕົ້ນ ແລະ ເອົາຂໍ້ຈຳກັດພອດ/ໂປຣໂຕຄອນທີ່ມີຢູໍ່ອກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"ປ່ຽນແທນຕົວເລືອກການດາວໂຫຼດທີ່ຖືກກຳນົດໄວ້ລ່ວງໜ້າ ສຳລັບເຄື່ອງມືດາວໂຫຼດທີ່ເລືອກ. ແຟລັກຜົນອອກ (output " +"flag), ເຊັ່ນ '-o' ສຳລັບ curl ຫຼື '-O' ສຳລັບ wget, ຈະຕ້ອງເປັນພາຣາມິເຕີຕົວສຸດທ້າຍ." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "ພາບລວມ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"ວິເຄາະສະເພາະລາຍການບັນທຶກຈຳນວນຫຼ້າສຸດທີ່ກຳນົດໄວ້ສຳລັບເຫດການທີ່ໜ້າສົງໄສ. " +"ຖ້າຕ້ອງການປິດການຕິດຕາມບັນທຶກ ໃຫ້ຕັ້ງເປັນ '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "ພອດ / ໂປຣໂຕຄອນ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "ບັນທຶກການປະມວນຜົນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "ໂປຣໄຟລ໌ທີ່ 'msmtp' ໃຊ້ສຳລັບອີເມວແຈ້ງເຕືອນ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - ໃຫ້ບໍລິການພາກພື້ນເອີຣົບ, ຕາເວັນອອກກາງ ແລະ ອາຊີກາງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"ອັດຕາ (ຕໍ່ວິນາທີ) ສຳລັບການຈຳກັດການບັນທຶກ NFT ທີ່ໃຊ້ຮ່ວມກັນ, ນຳໃຊ້ທົ່ວໂລກໃນທຸກກົດທີ່ມີການບັນທຶກ. " +"ຕັ້ງເປັນ '0' ເພື່ອປິດການຈຳກັດອັດຕາທັງໝົດ, ເຊັ່ນ: ເມື່ອໃຊ້ ulogd ຫຼື ເຄື່ອງມືຈັດການບັນທຶກອື່ນໆ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "ຮັບອີເມວແຈ້ງເຕືອນທຸກຄັ້ງທີ່ banIP ເຮັດວຽກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "ທີ່ຢູື່ອີເມວຜູ້ຮັບສຳລັບການແຈ້ງເຕືອນ banIP, ຂໍ້ມູນນີ້ຈຳເປັນຕ້ອງມີເພື່ອເປີດໃຊ້ງານຟັງຊັນອີເມວ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "ໂຫຼດໃໝ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "ທະບຽນອິນເຕີເນັດລະດັບພາກພື້ນ (RIR)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"ສຳນວນປົກກະຕິ ເພື່ອກວດຈັບ IP ທີ່ໜ້າສົງໄສໃນບັນທຶກລະບົບ. ໃຊ້ IP ສຸດທ້າຍຂອງແຕ່ລະແຖວ; ຕື່ມ 'first:' " +"ໃສ່ທາງໜ້າສຳລັບຮູບແບບທີ່ແຫຼ່ງກຳເນີດຢູ່ທາງໜ້າ, ຕົວຢ່າງ: nginx." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "ໂທເຄັນທາງໄກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "ເຫດການບັນທຶກຈາກທາງໄກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "ໄດເຣັກທໍຣີລາຍງານ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "ຕົວນັບການລາຍງານ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "ຈຳກັດການເຂົ້າເຖິງອິນເຕີເນັດຈາກ/ເຖິງຈຳນວນ IP ທີ່ປອດໄພຈຳນວນໜ້ອຍ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "ຜົນລວມ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "ກົດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "ທຸງການເຮັດວຽກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "ຂໍ້ມູນການເຮັດວຽກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "ຂອບເຂດ SYN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ຂອບເຂດ SYN ເປັນແພັກເກັດຕໍ່ວິນາທີເພື່ອປ້ອງກັນການໂຈມຕີ WAN-DoS. ຖ້າຕ້ອງການປິດການປ້ອງກັນນີ້ " +"ໃຫ້ຕັ້ງເປັນ '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "ບັນທຶກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "ບັນທຶກ ແລະ ໂຫຼດໃໝ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "ບັນທຶກ ແລະ ຣີສະຕາດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "ຄົ້ນຫາ IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "ກຳລັງຄົ້ນຫາ, ກະລຸນາລໍຖ້າ..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "ຄົ້ນຫາ IP ທີ່ກຳນົດໃນຊຸດທີ່ກ່ຽວຂ້ອງກັບ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "ການຄົ້ນຫາໝົດເວລາ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "ເລືອກໜຶ່ງໃນເຄື່ອງມືດາວໂຫຼດທີ່ຕັ້ງໄວ້ກ່ອນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "ເລືອກອຸປະກອນເຄືອຂ່າຍ WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "ເລືອກອິນເຕີເຟດເຄືອຂ່າຍ WAN IPv4 ທີ່ມີເຫດຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "ເລືອກອິນເຕີເຟດເຄືອຂ່າຍ WAN IPv6 ທີ່ມີເຫດຜົນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "ທີ່ຢູື່ອີເມວຜູ້ສົ່ງສຳລັບອີເມວແຈ້ງເຕືອນ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "ຊຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "ເນື້ອຫາຊຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "ເນື້ອຫາຊຸດ..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "ການພະຍາຍາມໂຫຼດຊຸດຊ້ຳ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "ນະໂຍບາຍຊຸດ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "ການລາຍງານຊຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "ຂະໜາດແຍກຊຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "ລາຍລະອຽດຊຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "ຕັ້ງລຳດັບຄວາມສຳຄັນຂອງ NFT chain ພາຍໃນຕາຕະລາງ banIP, ຄ່າຕ່ຳໝາຍເຖິງຄວາມສຳຄັນສູງ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "ຕັ້ງນະໂຍບາຍ NFT ສຳລັບຊຸດທີ່ກ່ຽວຂ້ອງກັບ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "ຕັ້ງລະດັບ syslog ສຳລັບການບັນທຶກ NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "ການຕັ້ງຄ່າ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "ສະແດງເນື້ອຫາ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "ສະແດງສະເພາະອົງປະກອບຊຸດທີ່ມີການເຂົ້າເຖິງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "ແຍກຊຸດ ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "ແຍກຊຸດປະເທດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "ແຍກການໂຫຼດຊຸດພາຍນອກຫຼັງຈາກທຸກ n ສະມາຊິກເພື່ອປະຢັດ RAM, ປິດໃຊ້ງານໂດຍຄ່າເລີ່ມຕົ້ນ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "ອິນເຕີເຟດກະຕຸ້ນການເລີ່ມຕົ້ນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "ສະຖານະ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "ຢຸດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "ຊັບເນັດ (Subnet)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "ສະຫຼຸບປະເທດຕ່າງໆໂດຍອີງໃສ່ທະບຽນອິນເຕີເນັດລະດັບພາກພື້ນ (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "ຂໍ້ມູນລະບົບ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "ການຕັ້ງຄ່າຕາຕະລາງ/Chain" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "ໄດເຣັກທໍຣີປາຍທາງສຳລັບໄຟລ໌ຂໍ້ຜິດພາດທີ່ກ່ຽວຂ້ອງກັບ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "ໄດເຣັກທໍຣີປາຍທາງສຳລັບໄຟລ໌ລາຍງານທີ່ກ່ຽວຂ້ອງກັບ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "ໄດເຣັກທໍຣີປາຍທາງສຳລັບການສຳຮອງຂໍ້ມູນຟີດທີ່ຖືກບີບອັດ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "ລາຍການອະນຸຍາດໃຫຍ່ເກີນໄປ, ບໍ່ສາມາດບັນທຶກການແກ້ໄຂໄດ້." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "ລາຍການກີດກັນໃຫຍ່ເກີນໄປ, ບໍ່ສາມາດບັນທຶກການແກ້ໄຂໄດ້." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "ASNs ທີ່ເລືອກຈະຖືກເກັບໄວ້ໃນຊຸດແຍກຕ່າງຫາກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "ປະເທດທີ່ເລືອກຈະຖືກເກັບໄວ້ໃນຊຸດແຍກຕ່າງຫາກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "ຄວາມສຳຄັນທີ່ເລືອກຈະຖືກນຳໃຊ້ສຳລັບການປະມວນຜົນເບື້ອງຫຼັງຂອງ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "ຜົນລວມ Syslog, ຖືກກັ່ນກອງລ່ວງໜ້າສຳລັບຂໍ້ຄວາມທີ່ກ່ຽວຂ້ອງກັບ: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"ນີ້ແມ່ນລາຍການອະນຸຍາດ banIP ທ້ອງຖິ່ນທີ່ຈະອະນຸຍາດທີ່ຢູ່ MAC, IP ຫຼື ຊື່ໂດເມນບາງຢ່າງ.
" +"ໝາຍເຫດ: ເພີ່ມພຽງແຕ່ໜຶ່ງທີ່ຢູ່ MAC/IPv4/IPv6 ຫຼື ຊື່ໂດເມນຕໍ່ແຖວເທົ່ານັ້ນ. " +"ຮອງຮັບຊ່ວງໃນຮູບແບບ CIDR ແລະ ການຜູກມັດ MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"ນີ້ແມ່ນລາຍການບລັອກ banIP ທ້ອງຖິ່ນທີ່ຈະປ້ອງກັນທີ່ຢູ່ MAC, IP ຫຼື ຊື່ໂດເມນບາງຢ່າງ.
" +"ໝາຍເຫດ: ເພີ່ມພຽງແຕ່ໜຶ່ງທີ່ຢູ່ MAC/IPv4/IPv6 ຫຼື ຊື່ໂດເມນຕໍ່ແຖວເທົ່ານັ້ນ. " +"ຮອງຮັບຊ່ວງໃນຮູບແບບ CIDR ແລະ ການຜູກມັດ MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"ລາຍງານນີ້ສະແດງສະຖິຕິ NFT Set ລ່າສຸດ, ກົດປຸ່ມ 'Refresh' ເພື່ອຮັບລາຍງານໃໝ່. " +"ທ່ານຍັງສາມາດສະແດງເນື້ອຫາສະເພາະຂອງຊຸດ, ຄົ້ນຫາ IP ທີ່ໜ້າສົງໄສ ແລະ ສຸດທ້າຍ, IP " +"ເຫຼົ່ານີ້ສາມາດສະແດງເທິງແຜນທີ່ໄດ້." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "ເວລາ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"ເພື່ອເປີດໃຊ້ງານການແຈ້ງເຕືອນທາງອີເມວ, ໃຫ້ຕັ້ງຄ່າແພັກເກັດ 'msmtp' ແລະ ລະບຸທີ່ຢູື່ອີເມວຜູ້ຮັບທີ່ຖືກຕ້ອງ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "ໂທເຄັນສຳລັບສື່ສານກັບອິນເຕີເຟດ cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "ຫົວຂໍ້ສຳລັບອີເມວແຈ້ງເຕືອນ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "ເວລາໜ່ວງກະຕຸ້ນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "ຂອບເຂດ UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ຂອບເຂດ UDP ເປັນແພັກເກັດຕໍ່ວິນາທີເພື່ອປ້ອງກັນການໂຈມຕີ WAN-DoS. ຖ້າຕ້ອງການປິດການປ້ອງກັນນີ້ " +"ໃຫ້ຕັ້ງເປັນ '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "ບໍ່ສາມາດວິເຄາະຂໍ້ມູນການເຮັດວຽກຂອງ banIP ໄດ້!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "ບໍ່ສາມາດວິເຄາະໄຟລ໌ປະເທດໄດ້!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "ບໍ່ສາມາດວິເຄາະໄຟລ໌ຟີດທີ່ກຳນົດເອງ!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "ບໍ່ສາມາດວິເຄາະໄຟລ໌ຟີດເລີ່ມຕົ້ນ!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "ບໍ່ສາມາດວິເຄາະໄຟລ໌ກົດລະບຽບໄດ້!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "ບໍ່ສາມາດບັນທຶກການແກ້ໄຂ: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "ອັບໂຫຼດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "ການອັບໂຫຼດໄຟລ໌ຟີດທີ່ກຳນົດເອງລົ້ມເຫຼວ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "ການບັນທຶກດີບັກລະອຽດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"ດ້ວຍຕົວແກ້ໄຂນີ້ ທ່ານສາມາດອັບໂຫຼດໄຟລ໌ຟີດກຳນົດເອງຂອງທ່ານ ຫຼື ຕື່ມຂໍ້ມູນເລີ່ມຕົ້ນ (ສຳເນົາ 1:1 " +"ຈາກເວີຊັນທີ່ມາພ້ອມກັບແພັກເກັດ). ໄຟລ໌ແມ່ນຢູ່ທີ່ '/etc/banip/banip.custom.feeds'. " +"ຈາກນັ້ນທ່ານສາມາດແກ້ໄຂໄຟລ໌ນີ້, ລຶບລາຍການ, ເພີ່ມລາຍການໃໝ່ ຫຼື ເຮັດການສຳຮອງຂໍ້ມູນທ້ອງຖິ່ນ. " +"ຖ້າຕ້ອງການກັບໄປໃຊ້ເວີຊັນຂອງຜູ້ພັດທະນາ ພຽງແຕ່ລ້າງໄຟລ໌ຟີດກຳນົດເອງອອກ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "ແຈ້ງເຕືອນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "ບັນຊີ asterisk ບໍ່ຖືກຕ້ອງ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "IPs ທີ່ຖືກເພີ່ມເຂົ້າລາຍການອະນຸຍາດໂດຍອັດຕະໂນມັດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "IPs ທີ່ຖືກເພີ່ມເຂົ້າລາຍການບລັອກໂດຍອັດຕະໂນມັດ" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "ແພັກເກັດ bcp38 ທີ່ຖືກບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "ແພັກເກັດ icmp-flood ທີ່ຖືກບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "ແພັກເກັດ ct ທີ່ບໍ່ຖືກຕ້ອງທີ່ຖືກບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "ແພັກເກັດ tcp ທີ່ບໍ່ຖືກຕ້ອງທີ່ຖືກບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "ແພັກເກັດ syn-flood ທີ່ຖືກບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "ແພັກເກັດ udp-flood ທີ່ຖືກບລັອກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "ວິກິດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "ດີບັກ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "ຖິ້ມ (Drop)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "ການເຂົ້າສູ່ລະບົບ dropbear ລົ້ມເຫຼວ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "ສຸກເສີນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "ຂໍ້ຜິດພາດ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "ຂໍ້ມູນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "ລາຍການອະນຸຍາດທ້ອງຖິ່ນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "ລາຍການບລັອກທ້ອງຖິ່ນ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "ໜ່ວຍຄວາມຈຳ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "ແຈ້ງການ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "ເອກະສານອອນລາຍ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "ຂໍ້ຜິດພາດ TLS ຂອງ openvpn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "ປະສິດທິພາບ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "ປະຕິເສດ (Reject)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd ປິດການເຊື່ອມຕໍ່" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "ການເຂົ້າສູ່ລະບົບ sshd ລົ້ມເຫຼວ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "ເຕືອນ" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "Regular expressions ເພື່ອກວດຫາ IP ທີ່ໜ້າສົງໄສໃນບັນທຶກລະບົບ." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "ຂຽນທັບຕົວເລືອກການດາວໂຫຼດທີ່ຕັ້ງໄວ້ກ່ອນໜ້າສຳລັບເຄື່ອງມືດາວໂຫຼດທີ່ເລືອກ." diff --git a/luci-app-secbox/po/lt/secbox.po b/luci-app-secbox/po/lt/secbox.po new file mode 100644 index 000000000..59cb4fd0d --- /dev/null +++ b/luci-app-secbox/po/lt/secbox.po @@ -0,0 +1,1567 @@ +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"PO-Revision-Date: 2026-05-25 08:35+0000\n" +"Last-Translator: Džiugas Januševičius \n" +"Language-Team: Lithuanian \n" +"Language: lt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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" +"X-Generator: Weblate 2026.6.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Prašome pasirinkti (pasirinktinis) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Nustatyti pasirinkimą --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- numatytasis/-oji --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "<„CSV“-skyriklis>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" +"" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "<„Suricata“ sintaksė>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "„AFRINIC“ – teikia Afrikos ir Indijos vandenyno regioną" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "„APNIC“ – teikia Azijos, Ramiojo vandenyno regioną" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "„ARIN“ – teikia Kanadą ir JAV" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "„ASN“ pasirinkimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "Autonominės sistemos („ASNs“)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktyvūs įrenginiai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Aktyvūs padavimai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktyvus aukštynkryptis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "„AdGuardHome“ prisijungimo klaida" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Papildomas suaktyvinimo atidėjimas sekundėmis, prieš pradedant „banIP“ " +"apdorojimą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Pažangūs nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Leisti protokolą/prievadus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Leisti „VLAN“ persiuntimus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Leidžiamojo sąrašo padavimo „URL“ – saitai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Tik leidžiamas sąrašas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Leidžiamojo sąrašo redagavimai buvo išsaugoti, perleiskite „banIP“, kad " +"pakeitimai įsigaliotų." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Visada leisti protokolą („TCP/UDP“) su tam tikrais prievadais arba prievadų " +"diapazonais „WAN“ įvesties ir „WAN“ persiuntimo grandinėse." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Visada leisti kai kuriuos „VLAN“ persiuntimus." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Visada blokuoti kai kuriuos „VLAN“ persiuntimus." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Automatinio leidžiamojo sąrašo aukštynkryptis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automatiškas leidžiamas sąrašas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Automatiškai blokuoti potinklį/-ius" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automatiškas blokavimo sąrašas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatinis patikrinimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Automatiškai pridėti ištisus potinklius į blokavimo sąrašą. Nustatyti pagal " +"papildomą „RDAP“ užklausą su įtartinu IP adresu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Automatiškai pridėti išspręstus domenus-sritys ir įtartinus IP adresus į " +"vietinį „banIP“ blokavimo sąrašą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Automatiškai pridėti išspręstus domenus ir aukštynkripties IP adresus į " +"vietinį „banIP“ leidžiamąjį sąrašą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Atsarginės kopijos katalogas/vietovė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Bazinis/-ė katalogas/vietovė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Bazinis/-ė darbinis/-ė katalogas/vietovė, apdorojant – „banIP“." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Blokuoti „VLAN“ persiuntimus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Blokuoti paketus su apsimetančiais šaltinio IP adresais visose palaikomose " +"grandinėse." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Draudžiamojo sąrašo padavimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Nustatyti blokavimo sąrašo galiojimo laiką" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Blokavimo sąrašo redagavimai buvo išsaugoti, perleiskite „banIP“, kad " +"pakeitimai įsigaliotų." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Bendrinamo „NFT“ žurnalo ribos paketų pliūpsnio dydis." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "„CPU“ branduoliai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Atšaukti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Grandinė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Grandinės pirmenybė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Pakeitimai, kuriuos šiame skirtuke atlikote, reikalauja tarnybos perleidimo, " +"kad būtų pritaikyti „banIP“ tarnybai." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Pakeitimai, kuriuos šiame skirtuke atlikote, reikalauja tarnybos paleidimo " +"iš naujo, kad būtų pritaikyti „banIP“ tarnybai." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Išvalyti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "IP adresų kolekcija pagal autonominių sistemų numerius („ASN“)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"„banIP“ prog. įrang. paketo konfigūracija, skirta uždrausti gaunamus ir " +"išsiunčiamus IP adresus naudojant įvardytus „nftables“ rinkinius. Norint " +"gauti daugiau informacijos, prašome patikrinti – %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Suskaičiuoti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Šalys" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Šalies pasirinkimas" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Pasirinktinis padavimo redaktorius" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Dedubliuoti IP adresus visuose aktyviuose rinkiniuose ir sutvarkyti vietinę " +"blokavimo sąrašą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Dedubliuoti IP(dgs.)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Aprašas/-ymas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Aptikti aktualius/reikšmingus tinklo įrenginius, sąsajas ir sietuvus, " +"potinklius, protokolus ir įrankius automatiškai." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Išjungti/Išgalinti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Netikrinti „SSL“ serverio sertifikatus, kol siunčiasi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Atsisiųsti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Atsisiuntimas nesaugus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Atsisiuntimo parametrai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Atsisiuntimo pakartojimai/bandymai iš naujo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Atsisiuntimo įrankis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Tyliai mesti/šalinti paketus arba aktyviai atmesti atvykstančią srautą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "El. pašto pranešimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "El. pašto profilis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "El. pašto gavėjo adresas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "El. pašto siuntėjo adresas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "El. pašto nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "El. pašto tema" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Redaguoti leidžiamąjį sąrašą" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Redaguoti draudžiamąjį sąrašą" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Elementų kiekis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elementai (maks. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Tuščias laukas nėra leidžiamas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Įjungti/Įgalinti „BCP38“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Įjungti/Įgalinti „GeoIP“ žemėlapį" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Įjungti „NFT“ skaitiklius rinkinio elementams ir grandinės taisyklėms. " +"Reikalinga „GeoIP“ žemėlapiui ir paketų statistikai rinkinio ataskaitose." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Įjungti/Įgalinti nuotolinį žurnalinimą" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Įjungti/Įgalinti „GeoIP“ žemėlapį su įtartinais rinkinio elementais. Tam " +"reikalingos išorinės užklausos, norint gauti žemėlapio išklotinės ir " +"geografinės vietos duomenys." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Įjungti „banIP“ tarnybą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" +"Įjunkite/Įgalinkite „cgi“ sąsają, kad gautumėte nuotolinio žurnalinimo " +"įvykius." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Įjungti/Įgalinti plačią/išsamią/daugiažodišką derinimo/trukdžių šalinimo " +"žurnalinimą klaidų apdorojimo atveju." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Įjungta/Įgalinta (-s/-i)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Įgalina IPv4 palaikymą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Įgalina IPv6 palaikymą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Klaidos katalogas/vietovė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Galiojimo laikas, skirtai automatiškai pridėto blokavimo sąrašo rinkinio " +"nariams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Išoriniai leidžiamojo sąrašo padavimai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Išoriniai blokavimo sąrašo padavimai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Nepavyko sugeneruoti „banIP“ atskaitos!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Padavimas užbaigtas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Padavimo vėliavos grąžinimas į pradinę padėtį ar būseną" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Padavimo pavadinimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Padavimo pasirinkimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Padavimo/Rinkinio nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Užpildyti" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Užkardos žurnalas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Vėliava" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Vėliava nepalaikoma" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Bendri nustatymai" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Duoti prieigą prie „LuCI-app-banIP“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Aukšta pirmenybė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Aukščiausia pirmenybė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "„ICMP-slenkstis“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"„ICMP“ – paketų per sekundę slenkstis, skirtas išvengti kelią „WAN-DoS“ " +"atakoms. Norėdami išjungti/išgalinti šią apsaugos priemonę, nustatykite ją į " +"– „0-į“." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP paieška" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP paieška..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4 tinklo sąsajos ir sietuvai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 palaikymas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6 tinklo sąsajos ir sietuvai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 palaikymas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Atvykstantis/-ys" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Atvykstantis/-ys ir išsiunčiami" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Atvykstantys ir išsiunčiami padavimai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Atvykstanti blokavimo politika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Atvykstantis padavimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Atvykstantys (paketai)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Padidinti maksimalų atidarytų failų skaičių, pvz., kad būtų galima tvarkyti " +"laikinai padalintų failų kiekį įkeliant rinkinius." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informacija" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Negalimas „URL“ – saito formatas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Negalimos rašmenys" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Negalimas galiojimo formatas, pvz., 5m, 2h, 1d arba 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Negalimos įvesties reikšmės, nepavyko išsaugoti pakeitimų/redagavimų." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "„LACNIC“ – teikia Lotynų Amerikos ir Karibų regioną" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Paskutinį kartą vykdytą" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Mažiausia pirmenybė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Mažesnė pirmenybė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Apriboti „banIP“ naudojamus „CPU“ branduolius, kad sutaupytumėte operatyvią " +"atmintį „RAM“, pagal numatytuosius nustatymus automatiškai aptinkama." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Apriboti aukštynkrypties automatinio leidimo funkciją." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Galimų tinklo sąsajų ir/arba sietuvų, skirtų suaktyvinti „banIP“ paleidimą, " +"sąrašas." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Išvardyti konkretaus su „banIP“ susijusio rinkinio elementus." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Vietinio padavimo nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Žurnalo failo nagrinėjimo vieta, pvz., naudojant – „syslog-ng“, kad būtų " +"išjungtas/išgalintas standartinis nagrinėjimas naudojant – „logread“." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Žurnalo pliūpsnio riba" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Žurnalo kiekis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Žurnalinti atvykstanti/-čius" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Žurnalo riba" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Žurnalinti išsiunčianti/-mus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Preliminarios kelvados žurnalas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Žurnalo spartos riba" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Žurnalo nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Žurnalinimo sąlygos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Žurnalinti įtartinus paketus „LAN-Forward“ grandinėje." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Žurnalinti įtartinus paketus preliminarios kelvados grandinėje." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Žurnalinti įtartinus paketus „WAN-Input“ ir „WAN-Forward“ grandinėse." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Žurnalo failo vietovė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "„LuCI“ nevykęs prisijungimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Žemėlapio grąžinimas į pradinę padėtį ar būseną" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Žemėlapis..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Maksimalus atidarytų failų kiekis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "„NFT“ informacija" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "„NFT“ žurnalo lygis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Tinklo įrenginiai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Tinklo klaida" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Lygis – „Nice“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Kol kas nėra %s susijusių žurnalų!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Nėra „GeoIP“ žemėlapio duomenų!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Nerasta „banIP“ konfigūracija!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normali pirmenybė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Nustatytų rinkinio įkėlimo bandymų skaičius klaidos atveju." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Atsisiuntimo bandymų skaičius klaidos atveju (nepalaikoma „uclient-fetch“)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Nepavykusių prisijungimų skaičius to pačio IP, kuris randamas žurnale prieš " +"blokavimą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Atsisakyti konkrečių padavimų iš dedubliavimo vyksmo." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Išsiunčiama/-s/-i" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Išsiunčiamas padavimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Išsiunčiama/-s/-i (paketai)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Perkeisti/Perrašyti numatytą padavimo konfigūraciją ir pritaikyti padavimą " +"atvykstančiai ir išsiunčiamai grandinei." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Perkeisti/Perrašyti numatytą padavimo konfigūraciją ir pritaikyti padavimą " +"tik atvykstančiai grandinei." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Perkeisti/Perrašyti numatytą padavimo konfigūraciją ir pritaikyti padavimą " +"tik išsiunčiamai grandinei." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Perkeisti/Perrašyti numatytą padavimo konfigūraciją ir pašalinti esamus " +"prievado/protokolo apribojimus." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Perkeisti/Perrašyti iš anksto sukonfigūruotas atsisiuntimo parinktys, skirtą " +"atsisiuntimo įrankiui. Išvesties vėliava, pvz., „-o“, skirta – „curl“ arba „-" +"O“, skirta – „wget“, turi būti paskutinis parametras." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Apžiūra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Nagrinėti tik paskutinį nurodytą žurnalo įvesčių skaičių, ieškant įtartinų " +"įvykių. Norėdami visiškai išjungti/išgalinti žurnalo stebėjimą, nustatykite " +"reikšmę – „0-į“." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Prievado / protokolas" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Apdorojamas žurnalas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Profilis, kurį naudoja „msmtp“, skirta – „banIP“ el. pašto pranešimams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "„RIPE“ – teikia Europos, Vidurinių rytų ir centrinės Azijos regionus" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Bendrinamo „NFT“ žurnalo apribojimo sparta (per sekundę), taikoma visoms " +"žurnalinamoms taisyklėms. Nustatykite reikšmę į – „0-į“, kad visiškai " +"išjungtumėte/išgalintumėte spartos ribojimą, pvz., naudojant „ulogd“ ar " +"kitas naudotojo/vartotojo erdvės žurnalo dorokles." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Gauti el. pašto pranešimus su kiekvienu „banIP“ paleidimu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Gavėjo adresas, skirtas „banIP“ pranešimų el. paštams. Ši informacija " +"reikalinga el. pašto funkcionalumui." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Atnaujinti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Regioninis interneto registras" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Nuotolinis žetonas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Nuotolinis žurnalinimo įvykis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Atskaitos katalogas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Atskaitų skaitikliai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Apriboti prieigą prie interneto iš/į nedidelį skaičių saugių IP (dgs.)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Rezultatas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Taisyklė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Vykdymo vėliavos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Vykdymo informacija" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "„SYN-slenkstis“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"„SYN“ slenkstis paketais per sekundę, skirtas užkirsti kelią „WAN-DoS“ " +"atakoms. Norėdami išjungti/iįgalinti šią apsaugą, nustatykite ją į – „0-į“." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Išsaugoti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Išsaugoti ir perleisti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Išsaugoti ir paleisti iš naujo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Ieškoti IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Paieška yra vykdoma, prašome palaukti..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Ieškoti „banIP“ susijusių rinkinių konkrečiam IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Pasibaigę paieškos užklausos laikas." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Pasirinkti vieną iš anksto sukonfigūruotų atsisiuntimo įrankių." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Pasirinkite „WAN“ tinklo įrenginį/-ius." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" +"Pasirinkti loginę/-es/-ius „WAN“ IPv4 tinklo sąsają/-s ir/arba sietuvą/-us." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" +"Pasirinkti loginę/-es/-ius „WAN“ IPv6 tinklo sąsają/-s ir/arba sietuvą/-us." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Siuntėjo adresas, skirtas „banIP“ pranešimų el. paštams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Nustatyti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Nustatyti turinį" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Nustatyti turinį..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Nustatyti įkėlimo pakartojimus/badymus iš naujo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Nustatyti politiką" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Nustatyti atskaitavimą" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Nustatyti plyšio dydį" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Nustatyti išsamesnę informaciją" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Nustatyti „NFT“ grandinės pirmenybę „banIP“ lentelėje; mažesnės reikšmės " +"reiškia aukštesnę pirmenybę." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Nustatyti „NFT“ politiką, skirtiems „banIP“ susijusiems rinkiniams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Nustatyti „syslog“ lygį „NFT“ žurnalinimui." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Rodyti turinį" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Rodyti tik rinkinio elementus su sutapimais" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Padalinti „ASN“ rinkinį" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Padalinti šalių rinkinį" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Padalinti išorinį rinkinio įkėlimą po kiekvieno n nario, kad būtų sutaupyta " +"operatyviosios atminties „RAM“, pagal numatytuosius nustatymus išjungta/" +"neįgalinta." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Paleidimo suaktyvinimo sąsaja ir/arba sietuvas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Būklė/Būsena" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Stabdyti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Potinklis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "Šalių santrauka pagal regioninį interneto registrą („RIR“)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Sistemos informacija" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Lentelės/Grandinės nustatymai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" +"Taikomasis/-oji katalogas/vietovė, skirtas/-a „banIP“ susijusioms klaidų " +"failams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" +"Taikomasis/-oji katalogas/vietovė, skirtas/-a „banIP“ susijusioms atskaitų " +"failams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" +"Taikomasis/-oji katalogas/vietovė, skirtas/-a suspaustiems/-" +"oms┃suglaudintiems/-oms padavimų atsarginėms kopijoms." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"Leidžiamasis sąrašas yra per didelis, nepavyko/negalima išsaugoti pakeitimų/" +"redagavimų." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" +"Draudžiamasis sąrašas yra per didelis, nepavyko/negalima išsaugoti pakeitimų/" +"redagavimų." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Pasirinkti „ASN“ (dgs.) yra saugomi atskiruose rinkiniuose." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Pasirinktos šalys yra saugomos atskiruose rinkiniuose." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Pasirinkta pirmenybė bus naudojama „banIP“ foniniam apdorojimui." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"„Syslog“ išvestis, iš anksto filtruota pagal pranešimus, susijusius su: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Tai yra vietinis „banIP“ leidžiamasis sąrašas, kuris leis tam tikrus „MAC“, " +"IP adresus arba domenų-sričių pavadinimus.
Prašome atkreipti " +"dėmesį: į kiekvieną eilutę pridėkite tik vieną „MAC“/IPv4/IPv6 " +"adresą arba domeno-srities pavadinimą. Leidžiami „CIDR“ žymėjimo diapazonai " +"ir „MAC“/IP pririšimai." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Tai yra vietinis „banIP“ draudžiamasis sąrašas, kuris išvengs tam tikrus " +"„MAC“, IP adresus arba domenų-sričių pavadinimus.
Prašome " +"atkreipti dėmesį: į kiekvieną eilutę pridėkite tik vieną „MAC“/IPv4/" +"IPv6 adresą arba domeno-srities pavadinimą. Leidžiami „CIDR“ žymėjimo " +"diapazonai ir „MAC“/IP pririšimai." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Šiame apskaite rodo naujausia „NFT“ rinkinio statistika. Norėdami gauti " +"naują, paspauskite mygtuką „Atnaujinti“. Jūs taip pat galite rodyti savitą " +"rinkinių turinį, ieškoti įtartinų IP (dgs.) ir galiausiai šiuos IP (dgs.) " +"galima rodyti žemėlapyje." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Laiko žyma" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Kad įjungtumėte/įgalintumėte el. pašo pranešimus, nustatykite „msmtp“ paketą " +"ir nurodykite galimą el. pašto gavėjo adresą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Žetonas, skirtas komunikuoti su „cgi“ sąsaja ir/arba sietuvu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Tema, skirta „banIP“ pranešimams el. paštui/-ams." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Suaktyvinimo atidėjimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "„UDP-slenkstis“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"„UDP“ slenkstis paketais per sekundę, skirtas užkirsti kelią „WAN-DoS“ " +"atakoms. Norėdami išjungti/išgalinti šią apsaugą, nustatykite ją į – „0-į“." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "„URLv4“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "„URLv6“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Nepavyko išnagrinėti „banIP“ vykdančiąją informaciją!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Nepavyko išnagrinėti šalių failo!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Nepavyko išnagrinėti pasirinktinio padavimo failo!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Nepavyko išnagrinėti numatyto padavimo failo!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Nepavyko išnagrinėti taisyklių rinkinio failo!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Nepavyko išsaugoti pakeitimų/redagavimų: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Įkelti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Įkeliant pasirinktinį padavimo failą įvyko klaida." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Platus/Išsamus/Daugiažodiškas derinimo/trukdžių šalinimo žurnalinimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Naudodami šį redaktorių, Jūs galite įkelti savo vietinį pasirinktinio " +"padavimo failą arba užpildyti pradinį (1:1 versijos kopija, pateiktas su " +"prog. įrang. paketu). Failas yra randamas per – „/etc/banip/" +"banip.custom.feeds“. Tada Jūs galite redaguoti šį failą, ištrinti įvestys, " +"pridėti naujų arba sukurti vietinę atsarginę kopiją. Norėdami grįžti į " +"kūrėjo versiją, tiesiog išvalykite pasirinktinį padavimo failą." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "įspėjimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "„asterisk“ negalima paskyra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "Automatiškai pridėti IP (dgs.) į leidžiamąjį sąrašą" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "Automatiškai pridėti IP (dgs.) į draudžiamąjį sąrašą" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "„banIP“" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "užblokuoti „BCP38“ paketai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "blokuoti „icmp-flood“ paketai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "blokuoti negalimi „ct“ paketai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "blokuoti negalimi „tcp“ paketai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "blokuoti „syn-flood“ paketai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "blokuoti „udp-flood“ paketai" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "kritinė" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "derinimas/trukdžių šalinimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "mesti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "„dropbear“ nevykęs prisijungimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "kritinė būklė/avarinis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "klaida" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "informacija" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "vietinis leidžiamasis sąrašas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "vietinis blokavimo sąrašas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "atmintis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "pranešimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "internetinė dokumentacija" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "„openvpn TLS“ klaida" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "našumas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "atmesti" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "„sshd“ uždarytas ryšys" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "„sshd“ nevykęs prisijungimas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "įspėti" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Reguliariosios išraiškos, skirtos aptikti įtartinus IP (dgs.) sistemos " +#~ "žurnale." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Perrašyti jau sukonfigūruotus atsisiuntimo parinktis, pasirinktam " +#~ "atsisiuntimo įrankiui." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Įjungti/Įgalinti „nft“ skaitiklį kiekvienam rinkinio elementui." + +#~ msgid "Set Element Counter" +#~ msgstr "Nustatyti elementų skaitiklį" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Nustatyti „nft“ grandinės pirmenybę „banIP“ lentelės viduje, žemesnės " +#~ "reikšmės nurodo aukštesnę pirmenybę." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Nustatyti „nft“ politiką, skirta „banIP“ susijusiems rinkiniams." + +#~ msgid "" +#~ msgstr "<„CSV“-skirtukas>" + +#~ msgid "nginx suspicious IP" +#~ msgstr "„nginx“ įtartinas IP" + +#~ msgid " " +#~ msgstr "" +#~ " " + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Protokolo/„URL“ – Saito formatas nepalaikomas" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Norėdami įjungti/įgalinti el. pašto pranešimus, nustatykite „msmtp“ prog. " +#~ "įrang. paketą ir nurodykite tinkamą el. pašto gavėjo adresą." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Nepavyko išnagrinėti vykdančiosios informacijos!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "„banIP“ prog. įrang paketo konfigūracija, skirta uždrausti gaunamus ir " +#~ "išsiunčiamus IP adresus naudojant įvardytus „nftables“ rinkinius. Prašome " +#~ "apsilankyti – internetinėje " +#~ "dokumentacijoje, dėl išsamios informacijos." + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", „CSV“" + +#~ msgid ", substring" +#~ msgstr ", poeilutis" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", „nodups“" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", sujungtas" + +#~ msgid "NET" +#~ msgstr "TINKLAS" + +#~ msgid "YOUR" +#~ msgstr "JŪSŲ" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", „CSV“" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "„Rulev4“" + +#~ msgid "Rulev6" +#~ msgstr "„Rulev6“" + +#~ msgid "Version" +#~ msgstr "Versija" + +#~ msgid "logread not found on system." +#~ msgstr "„logread“ nerastas sistemoje." + +#~ msgid "Elements" +#~ msgstr "Elementai" diff --git a/luci-app-secbox/po/mr/secbox.po b/luci-app-secbox/po/mr/secbox.po new file mode 100644 index 000000000..4ff9ed7ef --- /dev/null +++ b/luci-app-secbox/po/mr/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2020-02-07 09:19+0000\n" +"Last-Translator: Prachi Joshi \n" +"Language-Team: Marathi \n" +"Language: mr\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.11-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "वर्णन" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "आढावा" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/ms/secbox.po b/luci-app-secbox/po/ms/secbox.po new file mode 100644 index 000000000..5cf97dfb7 --- /dev/null +++ b/luci-app-secbox/po/ms/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2025-04-22 05:24+0000\n" +"Last-Translator: Abdul Muizz Bin Abdul Jalil \n" +"Language-Team: Malay \n" +"Language: ms\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 5.11.1-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Direktori Sandaran" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Direktori asas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Keterangan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Dibolehkan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Muat naik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/nb_NO/secbox.po b/luci-app-secbox/po/nb_NO/secbox.po new file mode 100644 index 000000000..f41c2c358 --- /dev/null +++ b/luci-app-secbox/po/nb_NO/secbox.po @@ -0,0 +1,1328 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2024-01-21 14:17+0000\n" +"Last-Translator: Allan Nordhøy \n" +"Language-Team: Norwegian Bokmål \n" +"Language: nb_NO\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.4-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Avanserte innstillinger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Sikkerhetskopimappe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Avbryt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Beskrivelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Last ned usikkert" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Nedlastingsparametre" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Nedlastingsverktøy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-postprofil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-postsenderadresse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-postemne" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Påskrudd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Generelle innstillinger" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6-støtte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Sist kjørt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Oversikt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Gjenoppfrisk" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Rapportmappe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +#, fuzzy +msgid "Run Flags" +msgstr "Kjøringsflagg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Lagre" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Innstillinger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Utløserforsinkelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "Version" +#~ msgstr "Versjon" diff --git a/luci-app-secbox/po/nl/secbox.po b/luci-app-secbox/po/nl/secbox.po new file mode 100644 index 000000000..7f9d8bc61 --- /dev/null +++ b/luci-app-secbox/po/nl/secbox.po @@ -0,0 +1,1383 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-01-10 04:39+0000\n" +"Last-Translator: Marco Otto \n" +"Language-Team: Dutch \n" +"Language: nl\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.15.1\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Kies alstublieft (optioneel) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Selectie instellen --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- standaard --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN's" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Actieve apparaten" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Actieve feeds" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Actieve uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "Extra start-vertraging voordat banIP proces begint (secondes)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Geavanceerde instellingen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "VLAN-forwards toestaan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Alleen toelatingslijst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Uplink automatisch toestaan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automatische toelatingslijst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Subnet automatisch blokkeren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automatische blokkeerlijst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Autodetectie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Voeg automatisch hele subnetten toe aan de blokkeerlijst Set op basis van " +"een extra RDAP-verzoek met het verdachte IP-adres." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Voeg automatisch opgeloste domeinen en verdachte IP's toe aan de lokale " +"banIP-blokkeerlijst." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Voeg automatisch opgeloste domeinen en uplink-IP's toe aan de lokale banIP-" +"toelatingslijst." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Backup Directory" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Basismap" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Basiswerkmap tijdens banIP-verwerking." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Vervaldatum van blokkeerlijst ingesteld" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU-kernen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Annuleren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Reeks Prioriteit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Wijzigingen op dit tabblad hebben een herstart van de banIP-service nodig om " +"van kracht te worden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Landen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Aangepaste feededitor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "IP's ontdubbelen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Beschrijving" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Detecteer automatisch relevante netwerkapparaten, interfaces, subnetten, " +"protocollen en hulpprogramma's." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Uitschakelen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Tijdens download niet de SSL server certificaten controleren." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Onbeveiligd downloaden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parameters downloaden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Nieuwe pogingen downloaden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Download hulpprogramma" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "E-Mail notificatie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-Mail profiel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "E-Mail adres van ontvanger" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-Mail adres van verzender" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Email instellingen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-Mail onderwerp" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Toelatingslijst bewerken" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Blokkeerlijst bewerken" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Aantal elementen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Leeg veld niet toegestaan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Activeer de banIP service." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Schakel uitgebreide logboekregistratie voor foutopsporing, in geval van " +"verwerkingsfouten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Ingeschakeld" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Schakelt IPv4-ondersteuning in." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Schakelt IPv6-ondersteuning in." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "Vervaltijd voor automatisch toegevoegde blocklist Set-leden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Feednaam" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Feed selectie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Firewall-logboek" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Vlag" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Vlag niet ondersteund" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Algemene instellingen" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Geef toegang tot LuCI app banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Hoge prioriteit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Hoogste prioriteit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP zoeken" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP Zoeken..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 ondersteuning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 ondersteuning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Verhoog het maximale aantal geopende bestanden, b.v. om het aantal " +"tijdelijke gesplitste bestanden te verwerken tijdens het laden van de Sets." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informatie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Ongeldige tekens" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Ongeldige invoerwaarden, kan wijzigingen niet opslaan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Laatst gedraaid" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Laagste prioriteit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Lage prioriteit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Beperk de uplink autoallow-functie." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "Lijst van beschikbare netwerkinterfaces die banIP starten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" +"Maak een lijst van de elementen van een specifieke banIP-gerelateerde Set." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Aantal logboeken" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Log limiet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Logboek instellingen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Logboek termen (sleutelwoorden)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Max Open Bestanden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT Informatie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Netwerk apparaten" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Mooi niveau" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normale prioriteit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Aantal downloadpogingen in geval van een fout (niet ondersteund door uclient-" +"fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Aantal mislukte inlogpogingen van hetzelfde IP-adres in het logboek vóór " +"blokkering." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Overzicht" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Parseer alleen het laatst vermelde aantal logboekvermeldingen voor verdachte " +"gebeurtenissen. Om de logmonitor helemaal uit te schakelen, zet u deze op " +"'0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Logboek verwerken" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Profiel gebruikt voor 'msmtp' voor banIP E-Mail berichten/notificaties." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Ontvang e-mailmeldingen bij elke banIP-run." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Ontvangeradres voor banIP-notificatie-e-mails, deze informatie is nodig om e-" +"mailfunctionaliteit in te schakelen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Herladen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Rapportage directory" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "Beperk de internettoegang van/tot een klein aantal beveiligde IP's." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultaat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Vlaggen uitvoeren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informatie uitvoeren" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Opslaan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Zoek in de banIP-gerelateerde sets naar een specifiek IP-adres." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Selecteer een van de vooraf geconfigureerde downloadhulpprogramma's." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Selecteer het/de WAN-netwerkapparaat(en)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Selecteer de logische WAN IPv4-netwerkinterface(s)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Selecteer de logische WAN IPv6-netwerkinterface(s)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Afzender e-mailadres voor de banIP email meldingen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Instellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Beleid instellen" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Rapportage instellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Splitsgrootte instellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Details instellen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Stel het syslog-niveau in voor NFT-logboekregistratie." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Instellingen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Startup Trigger-interface" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Status" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Stop" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Subnet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Doelmap voor banIP-gerelateerde rapportbestanden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Doelmap voor back-ups van gecomprimeerde feeds." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"De toelatingslijst is te groot, wijzigingen kunnen niet worden opgeslagen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "De blokkeerlijst is te groot, kan wijzigingen niet opslaan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"De geselecteerde prioriteit wordt gebruikt voor banIP-achtergrondverwerking." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Tijdstempel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Onderwerp voor banIP notificatie e-mails." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Trigger vertraging" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Kan wijzigingen niet opslaan: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Het uploaden van het aangepaste feedbestand is mislukt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Uitgebreide logboekregistratie voor foutopsporing" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "alarm" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "debuggen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "tevoorschijn komen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "fout" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lokale toelatingslijst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lokale blokkeerlijst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "geheugen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "kennisgeving" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "prestatie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "waarschuwen" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Negeer de vooraf geconfigureerde downloadopties voor het geselecteerde " +#~ "downloadhulpprogramma." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Stel het nft-beleid in voor banIP-gerelateerde sets." + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Protocol/URL-formaat wordt niet ondersteund" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Om e-mailmeldingen in te schakelen, stelt u het 'msmtp'-pakket in en " +#~ "geeft u een geldig e-mailontvangeradres op." + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Versie" + +#~ msgid "Elements" +#~ msgstr "Elementen" diff --git a/luci-app-secbox/po/pl/secbox.po b/luci-app-secbox/po/pl/secbox.po new file mode 100644 index 000000000..5091f2f7d --- /dev/null +++ b/luci-app-secbox/po/pl/secbox.po @@ -0,0 +1,1557 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-24 15:14+0000\n" +"Last-Translator: Matthaiks \n" +"Language-Team: Polish \n" +"Language: pl\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Wybierz (opcjonalne) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Wybór zestawów --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- domyślne --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - obsługuje region Afryki i Oceanu Indyjskiego" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - obsługuje region Azji i Pacyfiku" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - obsługuje Kanadę i Stany Zjednoczone" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Wybór ASN-ów" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN-y" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktywne urządzenia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Aktywne źródła" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktywne łącze nadrzędne" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "AdGuardHome: błąd logowania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Dodatkowe opóźnienie wyzwalania w sekundach przed rozpoczęciem przetwarzania " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Ustawienia zaawansowane" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Zezwalaj na protokół/porty" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Zezwalaj na przekazywanie VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Adresy URL źródeł listy dozwolonych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Tylko lista dozwolonych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Modyfikacje listy dozwolonych zostały zapisane. Załaduj ponownie banIP, aby " +"zmiany zaczęły obowiązywać." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Zawsze zezwalaj na protokół (tcp/udp) z określonymi portami lub zakresami " +"portów w łańcuchu wejścia WAN i przekazywania WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Zawsze zezwalaj na przekazywanie niektórych VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Zawsze blokuj przekazywanie niektórych VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Automatycznie zezwalaj na łącze nadrzędne" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automatyczna lista dozwolonych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Automatyczne blokowanie podsieci" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automatyczna lista zablokowanych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatyczne wykrywanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Automatycznie dodawaj całe podsieci do zestawu listy zablokowanych na " +"podstawie dodatkowego żądania RDAP z podejrzanym adresem IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Automatycznie dodawaj rozpoznane domeny i podejrzane adresy IP do lokalnej " +"listy zablokowanych banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Automatycznie dodawaj rozpoznane domeny i adresy IP łącza nadrzędnego do " +"lokalnej listy dozwolonych banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Katalog kopii zapasowej" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Katalog główny" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Główny katalog roboczy podczas przetwarzania banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Blokuj przekazywanie VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Blokuj pakiety z fałszywymi adresami IP we wszystkich obsługiwanych " +"łańcuchach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Źródło listy zablokowanych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Wygaśnięcie zestawu listy zablokowanych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Modyfikacje listy zablokowanych zostały zapisane. Załaduj ponownie banIP, " +"aby zmiany zaczęły obowiązywać." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Rozmiar pakietów dla limitu współdzielonego dziennika NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Rdzenie procesora" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Anuluj" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Łańcuch" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Priorytet łańcucha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "Przeładuj usługę banIP, aby zmiany zostały wprowadzone." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "Restartuj usługę banIP, aby zmiany zostały wprowadzone." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Wyczyść" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Zbiór adresów IP oparty na numerach systemu autonomicznego (ASN)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Konfiguracja pakietu banIP w celu blokowania przychodzących i wychodzących " +"adresów IP za pomocą nazwanych zestawów nftables. Aby uzyskać więcej " +"informacji, zobacz %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Liczba" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Kraje" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Wybór krajów" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Edytor źródeł niestandardowych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Deduplikuj adresy IP we wszystkich aktywnych zestawach i uporządkuj lokalną " +"listę zablokowanych." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Deduplikacja adresów IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Opis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Automatycznie wykrywaj odpowiednie urządzenia sieciowe, interfejsy, " +"podsieci, protokoły i narzędzia." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Wyłącz" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Nie sprawdzaj certyfikatów SSL serwera podczas pobierania." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Pobierz" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Niezabezpieczone pobieranie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parametry pobierania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Ponowne próby pobierania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Narzędzie pobierania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "Odrzucaj pakiety po cichu lub aktywnie odrzucaj ruch przychodzący." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Powiadomienie e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Profil e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Adres e-mail odbiorcy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Adres e-mail nadawcy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Ustawienia e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Temat e-mail" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Edytuj listę dozwolonych" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Edytuj listę zablokowanych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Liczba elementów" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elementy (maks. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Puste pole jest niedozwolone" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Włącz BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Włącz mapę GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Włącz liczniki NFT dla elementów zestawu i reguł łańcucha. Wymagane dla mapy " +"GeoIP i statystyk pakietów w raportowaniu zestawu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Włącz zdalne rejestrowanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Włącz mapę GeoIP z podejrzanymi elementami zestawu. Wymaga to zewnętrznych " +"żądań, aby uzyskać kafelki mapy i dane geolokalizacyjne." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Włącz usługę banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Włącz interfejs cgi, aby otrzymywać zdarzenia zdalnego rejestrowania." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Włącz pełne rejestrowanie debugowania w przypadku błędów przetwarzania." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Włączone" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Włącza obsługę IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Włącza obsługę IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Katalog błędów" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Czas wygaśnięcia automatycznie dodawanych członków zestawu listy zablokowych." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Zewnętrzne źródła listy dozwolonych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Zewnętrzne źródła listy zablokowanych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Nie udało się wygenerować raportu banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Kompletne źródło" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Reset flagi źródła" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Nazwa źródła" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Wybór źródeł" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Ustawienia źródła/zestawu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Wypełnij" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Dziennik zapory" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Flaga" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Flaga jest nieobsługiwana" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Ustawienia główne" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Udziel dostępu do aplikacji LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Wysoki priorytet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Najwyższy priorytet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "Próg ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Próg ICMP w pakietach na sekundę, aby zapobiec atakom WAN-DoS. Aby wyłączyć " +"to zabezpieczenie, ustaw je na '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Wyszukiwanie IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Wyszukiwanie IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Interfejsy sieciowe IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Obsługa IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Interfejsy sieciowe IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Obsługa IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Przychodzący" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Przychodzący i wychodzący" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Źródło przychodzącego i wychodzącego" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Zasady blokowania przychodzącego" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Źródło przychodzącego" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Przychodzący (pakiety)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Zwiększ maksymalną liczbę otwartych plików, np. do obsługi liczby " +"tymczasowych plików podzielonych podczas ładowania zestawów." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informacje" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Niepoprawny format URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Nieprawidłowe znaki" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Nieprawidłowy format wygaśnięcia, np. 5m, 2h, 1d lub 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Nieprawidłowe wartości wejściowe, nie można zapisać zmian." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - obsługuje region Ameryki Łacińskiej i Karaibów" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Ostatnie uruchomienie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Najniższy priorytet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Mniejszy priorytet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Ogranicz liczbę rdzeni CPU używanych przez banIP, aby zaoszczędzić RAM. " +"Domyślnie wykrywane automatycznie." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Ogranicz funkcję automatycznego zezwalania na łącze nadrzędne." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Lista dostępnych interfejsów sieciowych wyzwalających uruchomienie banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Wymień elementy określonego zestawu związanego z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Ustawienia lokalnego źródła" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Lokalizacja do analizy pliku dziennika, np. poprzez syslog-ng, aby " +"dezaktywować standardowe analizowanie poprzez logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Limit serii dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Liczba dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Rejestruj przychodzące" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Limit dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Rejestruj wychodzące" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Rejestruj prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Limit szybkości dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Ustawienia dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Warunki dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Rejestruj podejrzane pakiety w łańcuchu LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Rejestruj podejrzane pakiety w łańcuchu Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Rejestruj podejrzane pakiety w łańcuchu WAN-Input i WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Lokalizacja pliku dziennika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "LuCI: nieudane logowanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Resetowanie mapy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Mapa..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Maksymalna liczba otwartych plików" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Informacje NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Poziom rejestrowania NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Urządzenia sieciowe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Błąd sieciowy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Poziom nice" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Brak jeszcze dzienników powiązanych z %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Brak danych mapy GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Nie znaleziono konfiguracji banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normalny priorytet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Liczba prób ładowania zestawu w przypadku wystąpienia błędu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Liczba prób pobierania w przypadku błędu (nieobsługiwane przez uclient-" +"fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Liczba nieudanych prób logowania z tego samego adresu IP w dzienniku przed " +"zablokowaniem." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Zrezygnuj z procesu deduplikacji określonych źródeł." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Wychodzący" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Źródło wychodzącego" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Wychodzący (pakiety)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Zastąp domyślną konfigurację źródła i zastosuj źródło do łańcucha " +"przychodzącego i wychodzącego." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Zastąp domyślną konfigurację źródła i zastosuj źródło tylko do łańcucha " +"przychodzącego." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Zastąp domyślną konfigurację źródła i zastosuj źródło tylko do łańcucha " +"wychodzącego." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Zastąp domyślną konfigurację źródła i usuń istniejące ograniczenia portu/" +"protokołu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Zastąp wstępnie skonfigurowane opcje pobierania dla wybranego narzędzia do " +"pobierania. Flaga wyjściowa, np. „-o” dla curl lub „-O” dla wget, musi być " +"ostatnim parametrem." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Przegląd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analizuj tylko ostatnią określoną liczbę wpisów w dzienniku pod kątem " +"podejrzanych zdarzeń. Aby w ogóle wyłączyć monitorowanie dziennika, ustaw na " +"'0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Port / Protokół" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Dziennik przetwarzania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Profil używany przez \"msmtp\" dla wiadomości e-mail z powiadomieniem banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - obsługuje Europę, Bliski Wschód i Azję Środkową" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Szybkość (na sekundę) dla limitu współdzielonego dziennika NFT, stosowanego " +"globalnie do wszystkich reguł logowania. Ustaw na „0”, aby całkowicie " +"wyłączyć limitowanie szybkości, np. podczas korzystania z ulogd lub innych " +"procedur obsługi dzienników w przestrzeni użytkownika." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Otrzymuj powiadomienia e-mail przy każdym uruchomieniu banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Adres odbiorcy wiadomości e-mail z powiadomieniem banIP, ta informacja jest " +"wymagana do włączenia funkcji e-mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Odśwież" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Regionalny Rejestr Internetowy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Wyrażenia regularne służące do wykrywania podejrzanych adresów IP w " +"dzienniku systemowym. Używa ostatniego adresu IP w wierszu; poprzedzaj " +"termin prefiksem „first:” w przypadku formatów, w których źródło jest " +"pierwsze, np. nginx." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Zdalny token" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Zdarzenie zdalnego rejestrowania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Katalog raportu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Liczniki raportowania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Ogranicz dostęp do Internetu z/do niewielkiej liczby bezpiecznych adresów IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Wynik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Reguła" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Flagi uruchomieniowe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informacje uruchomieniowe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "Próg SYN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Próg SVN w pakietach na sekundę, aby zapobiec atakom WAN-DoS. Aby wyłączyć " +"to zabezpieczenie, ustaw je na '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Zapisz" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Zapisz i przeładuj" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Zapisz i zrestartuj" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Wyszukaj IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Trwa wyszukiwanie, proszę czekać..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Przeszukaj zestawy związane z banIP dla określonego adresu IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Przekroczono limit czasu wyszukiwania." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Wybierz jedno ze wstępnie skonfigurowanych narzędzi do pobierania." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Wybierz urządzenie(-a) sieciowe WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Wybierz logiczne interfejsy sieciowe IPv4 WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Wybierz logiczne interfejsy sieciowe IPv6 WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Adres nadawcy wiadomości e-mail z powiadomieniem banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Zestaw" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Zawartość zestawu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Zawartość zestawu..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Ustaw ponowienia ładowania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Zasady zestawu" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Raportowanie zestawu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Rozmiar podziału zestawu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Szczegóły zestawu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Ustaw priorytet łańcucha NFT w tabeli banIP. Niższe wartości oznaczają " +"wyższy priorytet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Ustaw zasady NFT dla zestawów powiązanych z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Ustaw poziom dziennika syslog dla rejestrowania NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Ustawienia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Pokaż zawartość" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Pokaż tylko elementy zestawu z dopasowaniami" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Podziel zestaw ASN-ów" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Podziel zestaw krajów" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Podziel ładowanie zewnętrznego zestawu po każdych n elementach, aby " +"zaoszczędzić RAM. Domyślnie wyłączone." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interfejs wyzwalacza uruchamiania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Status" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Zatrzymaj" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Podsieć" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" +"Podsumowanie krajów na podstawie Regionalnego Rejestru Internetowego (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Informacje o systemie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Ustawienia tablicy/łańcucha" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Katalog docelowy plików błędów związanych z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Katalog docelowy dla plików raportów związanych z banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Katalog docelowy dla kopii zapasowych skompresowanych źródeł." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Lista dozwolonych jest za duża, nie można zapisać zmian." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Lista zablokowanych jest za duża, nie można zapisać zmian." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Wybrane ASN-y są przechowywane w oddzielnych zestawach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Wybrane kraje są przechowywane w oddzielnych zestawach." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Wybrany priorytet będzie używany do przetwarzania banIP w tle." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"Dane wyjściowe dziennika systemowego, wstępnie przefiltrowane pod kątem " +"komunikatów związanych z: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"To jest lokalna lista dozwolonych banIP, która zezwala na określone adresy " +"MAC, adresy IP lub nazwy domen.
Uwaga: dodaj dokładnie " +"jeden adres MAC/IPv4/IPv6 lub nazwę domeny na wiersz. Dozwolone są zakresy w " +"notacji CIDR i powiązania MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"To jest lokalna lista zablokowanych banIP, która blokuje określone adresy " +"MAC, adresy IP lub nazwy domen.
Uwaga: dodaj dokładnie " +"jeden adres MAC/IPv4/IPv6 lub nazwę domeny na wiersz. Dozwolone są zakresy w " +"notacji CIDR i powiązania MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Ten raport pokazuje najnowsze statystyki zestawu NFT, naciśnij przycisk " +"„Odśwież”, aby uzyskać nowe. Możesz również wyświetlić konkretną zawartość " +"zestawów, wyszukać podejrzane adresy IP, a na koniec te adresy IP można " +"również wyświetlić na mapie." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Sygnatura czasowa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Aby włączyć powiadomienia e-mail, skonfiguruj pakiet „msmtp” i podaj " +"prawidłowy adres odbiorcy poczty e-mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token do komunikacji z interfejsem cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Temat wiadomości e-mail z powiadomieniem banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Opóźnienie wyzwalacza" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Próg UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Próg UDP w pakietach na sekundę, aby zapobiec atakom WAN-DoS. Aby wyłączyć " +"to zabezpieczenie, ustaw je na '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Nie można przeanalizować informacji o środowisku wykonawczym banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Nie można przeanalizować pliku krajów!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Nie można przeanalizować pliku niestandardowych źródeł!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Nie można przeanalizować pliku domyślnych źródeł!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Nie można przeanalizować pliku zestawu reguł!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Nie można zapisać modyfikacji: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Wysyłanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Przesyłanie pliku źródeł niestandardowych nie powiodło się." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Pełne rejestrowanie debugowania" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Za pomocą tego edytora możesz przesłać lokalny plik źródeł niestandardowych " +"lub wypełnić plik początkowy (kopia 1:1 wersji dostarczonej w pakiecie). " +"Plik znajduje się pod adresem '/etc/banip/banip.custom.feeds'. Następnie " +"możesz edytować ten plik, usuwać i dodawać wpisy lub wykonać lokalną kopię " +"zapasową. Aby wrócić do wersji oryginalnej, po prostu wyczyść plik źródeł " +"niestandardowych." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "alarm" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "asterisk: nieprawidłowe konto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "automatycznie dodane adresy IP do listy dozwolonych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "automatycznie dodane adresy IP do listy zablokowanych" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "zablokowane pakiety bcp38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "zablokowane pakiety icmp-flood" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "zablokowane nieprawidłowe pakiety ct" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "zablokowane nieprawidłowe pakiety tcp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "zablokowane pakiety syn-flood" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "zablokowane pakiety udp-flood" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "krytyczny" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "debugowanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "porzucaj" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "dropbear: nieudane logowanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "ratunkowy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "błąd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lokalna lista dozwolonych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lokalna lista zablokowanych" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "pamięć" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "spostrzeżenie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "dokumentację online" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "openvpn: błąd TLS" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "wydajność" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "odrzucaj" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd: zamknięte połączenie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "sshd: nieudane logowanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "ostrzeżenie" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Wyrażenia regularne służące do wykrywania podejrzanych adresów IP w " +#~ "dzienniku systemowym." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Zastąp wstępnie skonfigurowane opcje pobierania dla wybranego narzędzia " +#~ "do pobierania." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Włącz licznik nft dla każdego elementu zestawu." + +#~ msgid "Set Element Counter" +#~ msgstr "Ustaw licznik elementów" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Ustaw priorytet łańcucha nft w tablicy banIP, niższe wartości oznaczają " +#~ "wyższy priorytet." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Ustaw zasady nft dla zestawów związanych z banIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx: podejrzany adres IP" + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Invalid expiry format" +#~ msgstr "Nieprawidłowy format wygaśnięcia" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Format protokołu/adresu URL jest nieobsługiwany" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Aby włączyć powiadomienia e-mail, skonfiguruj pakiet 'msmtp' i podaj " +#~ "prawidłowy adres e-mail odbiorcy." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Nie można przetworzyć informacji o wykonywanych działaniach!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Konfiguracja pakietu banIP w celu blokowania przychodzących i " +#~ "wychodzących adresów IP za pośrednictwem nazwanych zestawów nftables. Aby " +#~ "uzyskać więcej informacji, sprawdź dokumentację " +#~ "online" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", podciąg" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", bez duplikatów" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", połączony" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "YOUR" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Regułav4" + +#~ msgid "Rulev6" +#~ msgstr "Regułav6" + +#~ msgid "Version" +#~ msgstr "Wersja" + +#~ msgid "logread not found on system." +#~ msgstr "Nie znaleziono logread w systemie." + +#~ msgid "banIP Log" +#~ msgstr "Dziennik banIP" + +#~ msgid "banIP firewall Log" +#~ msgstr "Dziennik zapory banIP" + +#~ msgid "Elements" +#~ msgstr "Elementy" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "Mapa GeoIP nie jest włączona!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Nie można przeanalizować pliku raportu!" diff --git a/luci-app-secbox/po/pt/secbox.po b/luci-app-secbox/po/pt/secbox.po new file mode 100644 index 000000000..567e5035f --- /dev/null +++ b/luci-app-secbox/po/pt/secbox.po @@ -0,0 +1,1484 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-05-20 17:02+0000\n" +"Last-Translator: ssantos \n" +"Language-Team: Portuguese \n" +"Language: pt\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 2026.6.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Por favor, escolha (opcional) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Definir seleção --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- padrão --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - servindo a África e a região do Oceano Índico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - servindo a região Ásia Pacífica" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - servindo o Canadá e os Estados Unidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Seleção de ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aparelhos ativos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Fontes Ativas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Uplink ativo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Erro de login do AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Atraso de gatilho adicional em segundos antes do início do processamento de " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Definições Avançadas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Permitir Protocolo/Portos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Permitir encaminhamentos de VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URLs de feeds da lista de permissões" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Apenas a lista dos permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"As modificações na lista de permissões foram gravadas; recarregue o banIP " +"para as alterações entrarem em vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Sempre permitir um protocolo (tcp/udp) com portass determinadas ou " +"intervalos de portas nas cadeias WAN-Input e WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Permitir sempre determinados encaminhamentos de VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Bloquear sempre determinados encaminhamentos de VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Permitir uplink automático" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Lista automática dos permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Sub-rede de bloqueio automático" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Lista automática de bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Deteção automática" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Adicione automaticamente sub-redes inteiras ao conjunto da lista de bloqueio " +"com base numa solicitação adiciona RDAP com o IP suspeito." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Adicione automaticamente domínios resolvidos e IPs suspeitos à lista de " +"bloqueio local banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Adicione automaticamente domínios resolvidos e IPs de uplink à lista de " +"permissões banIP local." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Diretório do Backup" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Diretório base" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" +"Diretório principal de trabalho usado durante o processamento do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Bloquear encaminhamentos de VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Bloquear pacotes com endereços IP de origem falsificados em todas as cadeias " +"suportadas." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Fonte de Lista de Bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Expiração definida da lista de bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"As modificações na lista de bloqueio foram gravadas; recarregue o banIP para " +"as alterações entrarem em vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Tamanho da rajada em pacotes para o limite de log NFT partilhado." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Núcleos da CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Cancelar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Cadeia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Prioridade da cadeia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"As alterações nesta guia exigem recarregar o serviço banIP para entrarem em " +"vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"As alterações nesta guia precisam de uma reinicialização do serviço banIP " +"para entrar em vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Limpar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Coleção de endereços IP baseada em Números de Sistemas Autônomos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Configuração do pacote banIP para banir IPs de entrada e saída via Conjuntos " +"nftables nomeados. Para mais informações, por favor verifique a %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Contagem" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Países" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Seleção de País" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Editor de Fonte Personalizado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Elimine endereços IP em todos os conjuntos ativos e limpe a lista local de " +"bloqueio." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Eliminar IPs duplicados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Descrição" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Detecte os aparelhos relevantes de rede, as interfaces, as sub-redes, os " +"protocolos e os utilitários automaticamente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Desativar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Não verificar os certificados de SSL do servidor durante a descarrega." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Descarregar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Descarregar inseguro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parâmetros de Descarregamento" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Novas tentativas de download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Ferramenta para Descarregar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Descartar pacotes silenciosamente ou rejeitar ativamente o tráfego de " +"entrada." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notificação por e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Perfil de e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Endereço de e-mail do destinatário" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Endereço de e-mail do remetente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Configurações do e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Assunto do e-mail" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Editar Lista de Permissão" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Editar Lista de Bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Contagem dos elementos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elementos (máx. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Campo vazio não permitido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Ativar BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Ativar Mapa GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Ativar contadores NFT para elementos do conjunto e regras de cadeia. " +"Obrigatório para o mapa GeoIP e as estatísticas de pacotes no Relatório de " +"Conjuntos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Ativar o registo remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Ativar um Mapa GeoIP com elementos suspeitos do Conjunto. Isto requer " +"solicitações externas para obter os blocos do mapa e os dados de " +"geolocalização." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Ative o serviço banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Ativar a interface cgi para receber eventos de registo remotos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Ative o registo de depuração detalhado em caso de erros de processamento." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Ativado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Ativa o suporte IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Ativa o suporte IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Diretório de Erros" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Tempo de expiração para membros do conjunto de lista de bloqueio adicionados " +"automaticamente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Feeds Externos de Lista de Permissão" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Fontes Externas de Lista de Bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Falha ao gerar o relatório do banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Feed Concluído" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Redefinição da Sinalização do Feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Nome da Fonte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Seleção de Fonte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Configurações de Feed/Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Preencher" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Registo do firewall" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Bandeira" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Bandeira não suportada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Configurações gerais" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Conceder acesso à app banIP do LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Alta prioridade" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Prioridade máxima" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "Limite de ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Limite de ICMP em pacotes por segundo para prevenir ataques WAN-DoS. Para " +"desativar esta proteção, defina como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Busca IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Busca IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Interfaces de rede IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Suporte ao IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Interfaces de rede IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Suporte de IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Entrada & Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Feed de Entrada & Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Política de Bloqueio de Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Feed de Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Entrada (pacotes)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Aumente o número máximo de arquivos abertos, por ex. para lidar com a " +"quantidade de arquivos divididos temporários durante o carregamento dos " +"conjuntos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informação" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Formato de URL inválido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Caracteres inválidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Formato de expiração inválido (ex.: 5m, 2h, 1d ou 1h30m)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Valores de entrada inválidos, não é possível gravar as modificações." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC — a atender a região da América Latina e do Caribe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Última Execução" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Prioridade mínima" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Prioridade menor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Limitar os núcleos de CPU usados pelo banIP para economizar RAM; " +"autodetectado por padrão." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Limite a função de permissão automática de uplink." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Lista de interfaces de rede disponíveis para acionar o início do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Liste os elementos de um conjunto específico relacionado ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Configurações de Feed Local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Localização para analisar o ficheiro de registo, por exemplo, via syslog-ng, " +"para desativar a análise padrão via logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Limite de Rajada de Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Contagem dos registos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Registar Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Limite do Registo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Registar Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Registar Prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Limite de taxa de registo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Configurações do registo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Termos do registo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Registar pacotes suspeitos na cadeia LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Registar pacotes suspeitos na cadeia Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Registar pacotes suspeitos nas cadeias WAN-Input e WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Localização do ficheiro de registo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "falha de login no LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Repor mapa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Mapa..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Quantidade máxima de ficheiros abertos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Informação NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Nível de registos NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Dispositivos de rede" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Erro de rede" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nível de Nice" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Ainda não há registos relacionados a %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Sem dados do mapa GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Nenhuma configuração do banIP encontrada!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Prioridade normal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Quantidade de tentativas de carregamento do Conjunto em caso de erro." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Número de tentativas de download em caso de erro (não suportado pelo uclient-" +"fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Quantidade de tentativas de login com falha do mesmo IP no registo antes do " +"bloqueio." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Excluir feeds específicos do processo de deduplicação." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Feed de Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Saída (pacotes)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Substituir a configuração padrão do feed e aplicar o feed às cadeias de " +"entrada e saída." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Substituir a configuração padrão do feed e aplicar o feed apenas à cadeia de " +"entrada." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Substituir a configuração padrão do feed e aplicar o feed apenas à cadeia de " +"saída." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Substituir a configuração padrão do feed e remover as limitações existentes " +"de porta/protocolo." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Substituir as opções predefinidas de descargas para o utilitário de " +"descargas selecionado. O sinalizador de saída, por exemplo, «-o» para o curl " +"ou «-O» para o wget, deve ser o último parâmetro." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Visão geral" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analisar apenas o último número declarado de entradas de log para eventos " +"suspeitos. Para desativar o monitor de log, defina-o como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Porta / Protocolo" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Registo de processamento" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "O perfil usado pelo 'msmtp' para os e-mails de notificação do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE — a atender a Europa, o Oriente Médio e a Ásia Central" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Taxa (por segundo) para o limite de log NFT partilhado, aplicado globalmente " +"em todas as regras registadas. Definir como '0' para desativar a taxa de " +"limite inteiramente, por exemplo, ao usar ulogd ou outros manipuladores de " +"log do espaço de utilizador." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Receba notificações por e-mail a cada execução do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Endereço de e-mail do destinatário para as notificações do banIP, esta " +"informação é necessária para ativar a funcionalidade do e-mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Atualizar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Registo Regional da Internet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Token remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Evento de log remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Diretório de Relatórios" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Contadores de Relatórios" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Restrinja o acesso à Internet de/para uma pequena quantidade de IPs seguros." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Regra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Flags de Execução" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informações de execução" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "Limite de SYN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Limite de SYN em pacotes por segundo para prevenir ataques WAN-DoS. Para " +"desativar esta proteção, defina como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Salvar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Gravar e Recarregar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Gravar e Reiniciar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Pesquisar IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "A pesquisa está em andamento, aguarde..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Faça a busca de um IP específico nos conjuntos relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Tempo limite de pesquisa esgotado." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Selecione um dos utilitários de descarga pré-configurados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Selecione o(s) aparelho(s) da rede WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Selecione a(s) interface(s) lógica(s) da rede WAN IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Selecione a(s) interface(s) lógica(s) da rede WAN IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Endereço do remetente para os e-mails de notificação do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Definir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Conteúdo do Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Conteúdo do Conjunto..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Tentativas de Carregamento do Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Definir a política" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Definir o relatório" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Definir o tamanho da divisão" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Definir os detalhes" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Definir a prioridade da cadeia NFT dentro da tabela banIP, valores mais " +"baixos significa maior prioridade." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Defina a política nft para conjuntos relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Define o nível do syslog para os registos NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Configurações" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Mostrar Conteúdo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Mostrar apenas os elementos do Conjunto com acertos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Dividir Conjunto de ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Dividir Conjunto de Países" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Dividir o carregamento de Conjuntos externos a cada n membros para " +"economizar RAM; desativado por padrão." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interface do Gatilho de Inicialização" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Estado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Parar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Sub-rede" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "Resumo dos países com base no Registo Regional da Internet (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Informações do Sistema" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Configurações de Tabela/Cadeia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Diretório de destino para ficheiros de erro relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" +"Diretório de destino para os ficheiros do relatório relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Diretório de destino para os backups comprimidos do feed." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"A lista de permissões é muito grande, não é possível gravar as modificações." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" +"A lista de bloqueio é muito grande, não é possível gravar as modificações." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Os ASNs selecionados são armazenados em Conjuntos separados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Os países selecionados são armazenados em Conjuntos separados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"A prioridade selecionada será usada pelo banIP para o processamento em " +"segundo plano." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "A saída syslog, pré-filtrada para mensagens relacionadas a: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Esta é a lista de permissões local do banIP que permitirá determinados " +"endereços MAC, IP ou nomes de domínio.
Observação: " +"adicione apenas um endereço, nome ou domínio MAC/IPv4/IPv6 por linha. " +"Intervalos na notação CIDR e ligações MAC/IP são permitidos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Esta é a lista de bloqueio local do banIP que impedirá determinados " +"endereços MAC, IP ou nomes de domínio.
Observação: " +"adiciona exatamente um endereço MAC/IPv4/IPv6 ou nome de domínio por linha. " +"São permitidos intervalos na notação CIDR e associações de MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Este relatório mostra as estatísticas mais recentes dos Conjuntos NFT; " +"pressione o botão ‘Atualizar’ para obter um novo. Também pode exibir o " +"conteúdo específico dos Conjuntos, pesquisar por IPs suspeitos e, por fim, " +"estes IPs também podem ser exibidos num mapa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Marca de Tempo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Para ativar as notificações por e-mail, configure o pacote 'msmtp' e " +"especifique um endereço de e-mail de destinatário válido." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token para comunicar com a interface cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Tópico para e-mails de notificação do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Atraso do Gatilho" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Limite de UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Limite de UDP em pacotes por segundo para prevenir ataques WAN-DoS. Para " +"desativar esta proteção, defina como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" +"Não foi possível analisar as informações de tempo de execução do banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Não foi possível analisar o ficheiro de países!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Não foi possível interpretar o ficheiro de fonte personalizada!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Não foi possível interpretar o ficheiro de fonte padrão!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Não foi possível analisar o ficheiro de conjunto de regras!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Não foi possível gravar as modificações: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Upload" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "O envio do ficheiro de fonte personalizada falhou." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Registos detalhados de depuração" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Com este editor, pode enviar o seu ficheiro local de feed personalizado ou " +"preencher um ficheiro inicial (uma cópia 1:1 da versão fornecida com o " +"pacote). O ficheiro está localizado em ‘/etc/banip/banip.custom.feeds’. " +"Depois, pode editar este ficheiro, apagar entradas, adicionar novas ou fazer " +"um backup local. Para retornar à versão dos mantenedores, basta limpar o " +"ficheiro de feed personalizado." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "alerta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "conta inválida do asterisk" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "IPs adicionados automaticamente à lista de permissões" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "IPs adicionados automaticamente à lista de bloqueio" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "pacotes bcp38 bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "pacotes icmp-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "pacotes ct inválidos bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "pacotes TCP inválidos bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "pacotes syn-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "pacotes udp-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crítico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "detalhado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "descartar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "falha de login no dropbear" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "urgente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "erro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lista dos permitidos local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lista de bloqueio local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "memória" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "aviso" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "documentação online" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "erro de TLS no OpenVPN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "desempenho" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "rejeitar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "conexão fechada pelo sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "falha de login no sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "alertar" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Expressões regulares para detetar IPs suspeitos no registo do sistema." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Substitua as opções de descarga pré-configuradas para o utilitário de " +#~ "descarga selecionado." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Defina a política nft para conjuntos relacionados ao banIP." + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Formato de protocolo/URL não suportado" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Para ativar as notificações por e-mail, configure o pacote 'msmtp' e " +#~ "especifique um endereço de e-mail com um destinatário válido." + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", subcadeia" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Versão" + +#~ msgid "logread not found on system." +#~ msgstr "logread não foi encontrado no sistema." + +#~ msgid "Elements" +#~ msgstr "Elementos" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "O mapa GeoIP não está ativado!" diff --git a/luci-app-secbox/po/pt_BR/secbox.po b/luci-app-secbox/po/pt_BR/secbox.po new file mode 100644 index 000000000..f4102c29b --- /dev/null +++ b/luci-app-secbox/po/pt_BR/secbox.po @@ -0,0 +1,1553 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-17 03:02+0000\n" +"Last-Translator: Janderson Vieira Santos \n" +"Language-Team: Portuguese (Brazil) \n" +"Language: pt_BR\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Por favor, escolha (opcional) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Definir seleção --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- padrão --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC – atendendo a África e a região do Oceano Índico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC – atendendo à região da Ásia-Pacífico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN – atendendo ao Canadá e aos Estados Unidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Seleção de ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Dispositivos Ativos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Fontes Ativas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Enlace ativo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Erro de login do AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Atraso de disparo adicional em segundos antes do início do processamento de " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Configurações Avançadas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Permitir protocolo/portas" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Permitir encaminhamentos de VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URLs de feeds da lista de permissões" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Apenas a lista dos permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"As modificações na lista de permissões foram salvas; recarregue o banIP para " +"que as alterações entrem em vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Sempre permitir um protocolo (tcp/udp) com determinadas portas ou faixas de " +"portas nas cadeias WAN-Input e WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Sempre permita certos encaminhamentos da VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Sempre negue certos encaminhamentos da VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Permite o enlace automático" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Lista automática dos permitidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Sub-rede de bloqueio automático" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Lista automática de bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Detecção Automática" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Adicione automaticamente sub-redes inteiras ao conjunto da lista de bloqueio " +"com base numa solicitação adiciona RDAP com o IP suspeito." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Adicione automaticamente os domínios resolvidos e os IPs suspeitos à lista " +"de bloqueio local do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Adicione automaticamente os domínios resolvidos e os IPs do enlace à lista " +"de permissões locais do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Diretório da cópia de segurança" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Diretório base" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" +"Diretório principal de trabalho usado durante o processamento do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Bloquear os encaminhamentos da VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Bloquear pacotes com endereços IP de origem falsificados em todas as cadeias " +"suportadas." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Fonte de Lista de Bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Expiração do conjunto das listas de bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"As modificações na lista de bloqueio foram salvas; recarregue o banIP para " +"que as alterações entrem em vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Tamanho do burst em pacotes para o limite compartilhado de log do NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Núcleos da CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Cancelar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Cadeia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Prioridade da cadeia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"As alterações nesta aba exigem recarregar o serviço banIP para entrarem em " +"vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"As alterações nesta guia precisam de uma reinicialização do serviço banIP " +"para entrar em vigor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Limpar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Coleção de endereços IP baseada em Números de Sistemas Autônomos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Configuração do pacote banIP para banir IPs de entrada e saída via Conjuntos " +"nftables nomeados. Para mais informações, por favor verifique a %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Contagem" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Países" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Seleção de País" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Editor de Fonte Personalizado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Elimine endereços IP em todos os conjuntos ativos e limpe a lista local de " +"bloqueio." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Eliminar IPs duplicados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Descrição" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Detecte os dispositivos relevantes de rede, as interfaces, as sub-redes, os " +"protocolos e os utilitários automaticamente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Desativar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Não verifique os certificados do servidor SSL durante o download." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Download inseguro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parâmetros de download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Tentativas de download" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Ferramenta para Baixar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Descartar pacotes silenciosamente ou rejeitar ativamente o tráfego de " +"entrada." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notificação por E-Mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-Mail do Perfil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Endereço de E-Mail do Destinatário" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Endereço de E-Mail do Remetente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Configurações do e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Assunto do E-Mail" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Editar Lista de Permissão" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Editar Lista de Bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Contagem dos elementos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Elementos (máx. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Campo vazio não permitido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Ativar BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Ativar Mapa GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Ativar contadores NFT para elementos de Set e regras de chain. Necessário " +"para o Mapa GeoIP e para as estatísticas de pacotes no Relatório de Sets." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Ativar Registro Remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Ativar um Mapa GeoIP com elementos suspeitos do Conjunto. Isso requer " +"solicitações externas para obter os blocos do mapa e os dados de " +"geolocalização." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Ative o serviço banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Ativar a interface CGI para receber eventos de registro remoto." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Ative o registro de depuração detalhado em caso de erros de processamento." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Ativado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Ativa o suporte IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Ativa o suporte IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Diretório de Erros" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Tempo de expiração para os membros do conjunto da lista de bloqueio que " +"foram adicionados automaticamente." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Feeds Externos de Lista de Permissão" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Fontes Externas de Lista de Bloqueio" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Falha ao gerar o relatório do banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Feed Concluído" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Redefinição da Sinalização do Feed" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Nome da Fonte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Seleção de Fonte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Configurações de Feed/Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Preencher" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Registro do firewall" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Sinalizador" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Sinalizador não suportado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Configurações gerais" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Conceda acesso ao aplicativo LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Alta prioridade" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Prioridade máxima" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "Limite de ICMP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Limite de ICMP em pacotes por segundo para prevenir ataques WAN-DoS. Para " +"desativar essa proteção, defina como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Busca IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Busca IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Interfaces de Rede IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Suporte ao IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Interfaces de Rede IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Suporte ao IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Entrada & Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Feed de Entrada & Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Política de Bloqueio de Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Feed de Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Entrada (pacotes)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Aumente a quantidade máxima de arquivos abertos, por ex. para lidar com a " +"quantidade temporária de arquivos divididos durante o carregamento dos " +"conjuntos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informações" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Formato de URL inválido" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Caracteres inválidos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Formato de expiração inválido (ex.: 5m, 2h, 1d ou 1h30m)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Valores de entrada inválidos, não é possível salvar as modificações." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC — atendendo a região da América Latina e do Caribe" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Última Execução" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Prioridade Mínima" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Menos prioridade" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Limitar os núcleos de CPU usados pelo banIP para economizar RAM; " +"autodetectado por padrão." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Limite a função de permissão automática do enlace." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Lista de interfaces de rede disponíveis para acionar o início do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Liste os elementos de um conjunto específico relacionado ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Configurações de Feed Local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Local para análise do arquivo de logs, por exemplo via syslog-ng, para " +"desativar a análise padrão via logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Limite de Burst do Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Contagem dos registros" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Registrar Entrada" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Limite de Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Registrar Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Registrar Prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Limite de Taxa do Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Configurações do registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Termos do registro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Registrar pacotes suspeitos na cadeia LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Registrar pacotes suspeitos na cadeia Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Registrar pacotes suspeitos nas cadeias WAN-Input e WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Local do Arquivo de Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "falha de login no LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Redefinir mapa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Mapa..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Quantidade máxima de arquivos abertos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Informação NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Nível de registro NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Dispositivos de rede" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Erro de rede" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nível adequado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Ainda não há logs relacionados a %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Nenhum dado no Mapa GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Nenhuma configuração do banIP encontrada!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Prioridade normal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Quantidade de tentativas de carregamento do Conjunto em caso de erro." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Número de tentativas de download em caso de erro (não suportado pelo uclient-" +"fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Quantidade de tentativas de login com falha do mesmo IP no registro antes do " +"bloqueio." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Excluir feeds específicos do processo de deduplicação." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Feed de Saída" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Saída (pacotes)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Substituir a configuração padrão do feed e aplicar o feed às cadeias de " +"entrada e saída." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Substituir a configuração padrão do feed e aplicar o feed apenas à cadeia de " +"entrada." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Substituir a configuração padrão do feed e aplicar o feed apenas à cadeia de " +"saída." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Substituir a configuração padrão do feed e remover as limitações existentes " +"de porta/protocolo." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Sobrescreve as opções de download pré-configuradas para o utilitário de " +"download selecionado. A flag de saída, ex: '-o' para o curl ou '-O' para o " +"wget, deve ser o último parâmetro." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Visão geral" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analise apenas o último número indicado das entradas de registro para os " +"eventos suspeitos. Para desativar o registro de monitoramento, defina-o como " +"'0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Porta / Protocolo" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Registro de processamento" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "O perfil usado pelo 'msmtp' para os e-mails de notificação do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE — atendendo a Europa, o Oriente Médio e a Ásia Central" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Taxa (por segundo) para o limite compartilhado de log do NFT, aplicada " +"globalmente a todas as regras que geram log. Defina como '0' para desativar " +"totalmente o limite de taxa, ex: ao usar o ulogd ou outros manipuladores de " +"log em espaço do usuário (userspace)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Receba notificações por e-mail a cada execução do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Endereço de e-mail do destinatário para as notificações do banIP, esta " +"informação é necessária para ativar a funcionalidade do e-mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Atualizar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Registro Regional da Internet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Token Remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Evento de log remoto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Diretório do Relatório" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Contadores de Relatório" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Restrinja o acesso à Internet de/para uma pequena quantidade de IPs seguros." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Regra" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Executar Flags" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informações de execução" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "Limite de SYN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Limite de SYN em pacotes por segundo para prevenir ataques WAN-DoS. Para " +"desativar essa proteção, defina como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Salvar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Salvar e Recarregar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Salvar e Reiniciar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Pesquisar IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "A pesquisa está em andamento, aguarde..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Faça a busca de um IP específico nos conjuntos relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Tempo limite de pesquisa esgotado." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Selecione um dos utilitários de download pré-configurados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Selecione o(s) dispositivo(s) da rede WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Selecione a(s) interface(s) lógica(s) da rede WAN IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Selecione a(s) interface(s) lógica(s) da rede WAN IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Endereço do remetente para os e-mails de notificação do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Definir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Conteúdo do Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Conteúdo do Conjunto..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Tentativas de Carregamento do Conjunto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Definir a política" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Definir o relatório" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Definir o tamanho da divisão" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Definir os detalhes" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Define a prioridade da chain NFT dentro da tabela banIP; valores menores " +"significam maior prioridade." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Define a política NFT para os Sets relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Define o nível do syslog para os registros NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Configurações" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Mostrar Conteúdo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Mostrar apenas os elementos do Conjunto com acertos" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Dividir Conjunto de ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Dividir Conjunto de Países" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Dividir o carregamento de Conjuntos externos a cada n membros para " +"economizar RAM; desativado por padrão." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interface do Gatilho de Inicialização" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Status" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Parar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Sub-rede" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "Resumo dos países com base no Registro Regional da Internet (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Informações do Sistema" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Configurações de Tabela/Cadeia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Diretório de destino para arquivos de erro relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" +"Diretório de destino para os arquivos do relatório relacionados ao banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Diretório de destino para os backups comprimidos do feed." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"A lista de permissões é muito grande, não é possível salvar as modificações." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" +"A lista de bloqueio é muito grande, não é possível salvar as modificações." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Os ASNs selecionados são armazenados em Conjuntos separados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Os países selecionados são armazenados em Conjuntos separados." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"A prioridade selecionada será usada pelo banIP para o processamento em " +"segundo plano." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"A saída do syslog, previamente filtradas para mensagens relacionadas a: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Esta é a lista de permissões local do banIP que permitirá determinados " +"endereços MAC, IP ou nomes de domínio.
Observação: " +"adicione apenas um endereço, nome ou domínio MAC/IPv4/IPv6 por linha. " +"Intervalos na notação CIDR e ligações MAC/IP são permitidos." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Esta é a lista de bloqueio local do banIP que impedirá determinados " +"endereços MAC, IP ou nomes de domínio.
Observação: " +"adiciona exatamente um endereço MAC/IPv4/IPv6 ou nome de domínio por linha. " +"São permitidos intervalos na notação CIDR e associações de MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Este relatório mostra as estatísticas mais recentes dos Conjuntos NFT; " +"pressione o botão ‘Atualizar’ para obter um novo. Você também pode exibir o " +"conteúdo específico dos Conjuntos, pesquisar por IPs suspeitos e, por fim, " +"esses IPs também podem ser exibidos em um mapa." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Marca de Tempo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Para habilitar as notificações por e-mail, configure o pacote 'msmtp' e " +"especifique um endereço de e-mail de destinatário válido." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token para comunicar-se com a interface CGI." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Tópico para e-mails de notificação do banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Gatilho de Atraso" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Limite de UDP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"Limite de UDP em pacotes por segundo para prevenir ataques WAN-DoS. Para " +"desativar essa proteção, defina como '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" +"Não foi possível analisar as informações de tempo de execução do banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Não foi possível analisar o arquivo de países!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Não foi possível interpretar o arquivo de fonte personalizada!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Não foi possível interpretar o arquivo de fonte padrão!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Não foi possível analisar o arquivo de conjunto de regras!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Não foi possível salvar as modificações: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Upload" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "O envio do arquivo de fonte personalizada falhou." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Registros Detalhados de Depuração" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Com este editor, você pode enviar seu arquivo local de feed personalizado ou " +"preencher um arquivo inicial (uma cópia 1:1 da versão fornecida com o " +"pacote). O arquivo está localizado em ‘/etc/banip/banip.custom.feeds’. " +"Depois, você pode editar esse arquivo, excluir entradas, adicionar novas ou " +"fazer um backup local. Para retornar à versão dos mantenedores, basta limpar " +"o arquivo de feed personalizado." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "alerta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "conta inválida do asterisk" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "IPs adicionados automaticamente à lista de permissões" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "IPs adicionados automaticamente à lista de bloqueio" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "Banir IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "pacotes bcp38 bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "pacotes icmp-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "pacotes ct inválidos bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "pacotes TCP inválidos bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "pacotes syn-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "pacotes udp-flood bloqueados" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crítico" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "detalhado" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "descartar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "falha de login no dropbear" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "urgente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "erro" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lista dos permitidos local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lista de bloqueio local" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "memória" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "aviso" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "documentação online" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "erro de TLS no OpenVPN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "desempenho" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "rejeitar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "conexão fechada pelo sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "falha de login no sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "alertar" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Expressões regulares para detectar IPs suspeitos no registro do sistema." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Substitua as opções de download pré-configuradas para o utilitário de " +#~ "download selecionado." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Ativar contador nft para cada elemento do Conjunto." + +#~ msgid "Set Element Counter" +#~ msgstr "Contador de Elementos do Conjunto" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Definir a prioridade da cadeia nft dentro da tabela banIP; valores " +#~ "menores significam prioridade maior." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Defina a política NFT para os conjuntos relacionados ao banIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "IP suspeito no nginx" + +#~ msgid " " +#~ msgstr " " + +#~ msgid "Invalid expiry format" +#~ msgstr "Formato de expiração inválido" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Formato de protocolo/URL não suportado" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Para ativar as notificações por e-mail, configure o pacote 'msmtp' e " +#~ "especifique um endereço de e-mail com um destinatário válido." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Não foi possível analisar as informações de tempo de execução!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Configuração do pacote banIP para bloquear IPs de entrada e saída por " +#~ "meio de Conjuntos nomeados do nftables. Para mais informações, consulte a " +#~ "documentação online" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", substring" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", concatenado" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "YOUR" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Versão" + +#~ msgid "logread not found on system." +#~ msgstr "logread não encontrado no sistema." + +#~ msgid "Elements" +#~ msgstr "Elementos" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "O Mapa GeoIP não está ativado!" diff --git a/luci-app-secbox/po/ro/secbox.po b/luci-app-secbox/po/ro/secbox.po new file mode 100644 index 000000000..cb15dc512 --- /dev/null +++ b/luci-app-secbox/po/ro/secbox.po @@ -0,0 +1,1396 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-03-18 02:23+0000\n" +"Last-Translator: CRISTIAN ANDREI \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2;\n" +"X-Generator: Weblate 5.17-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Vă rugăm alegeți (opțional) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Selecția setului --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "--Implicit--" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN-uri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Dispozitive active" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Fluxuri active" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Uplink activ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Întârziere suplimentară declanșare in secunde înainte de începerea " +"procesării banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Setări Avansate" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Permiteți redirecționarea VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Doar Allowlist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Permiteți întotdeauna anumite transmisiuni VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Blocați întotdeauna anumite transmisiuni VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Permite automat legătura ascendentă" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Allowlist Automată" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Blocaj automat Subnet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Lista de blocare automată" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Detecție automată" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Adăugați automat subrețele întregi la lista de blocare Set bazat pe o cerere " +"RDAP suplimentară cu IP-ul suspect." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Adăugarea automată a domeniilor rezolvate și a IP-urilor suspecte la lista " +"de blocare banIP locală." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Adăugați automat domeniile rezolvate și IP-urile de legătură ascendentă la " +"lista de permise banIP locală." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Director copie de siguranţă" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Directorul de bază" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Directorul de lucru de bază în timpul procesării banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Blocați transmisiile VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Fluxul listei de blocare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Expirarea setului Blocklist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Nuclee CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Anulare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Prioritatea Chain" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Modificările din această filă necesită o repornire a serviciului banIP " +"pentru a intra în vigoare." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Curățare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Număr" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Țări" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Editor de flux personalizat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Deduplicați adresele IP în toate seturile active și faceți ordine în lista " +"de blocuri locale." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Deduplicați IP-uri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Descriere" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Detectarea automată a dispozitivelor de rețea, interfețelor, subrețelelor, " +"protocoalelor și utilităților relevante." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Dezactivați" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Nu verificați certificatele serverului SSL în timpul descărcării." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Descărcare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Descărcați Nesigure" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Parametrii de descărcare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Descărcați Reîncercări" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Utilitar descărcare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Renunțați la pachete silențios sau respingeți activ Traficul de intrare." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Notificare prin e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Profil de e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Adresa de e-mail a destinatarului" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Adresa expeditorului de e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Setări e-mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Subiect E-Mail" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Editați Lista Permise" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Editați Lista Blocare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Număr de elemente" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Câmpul gol nu este permis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Activați serviciul banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"Activați înregistrarea verbală a depanării în caz de erori de procesare." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Activat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Activează suportul IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Activează suportul IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "Timpul de expirare pentru membrii setului blocklist adăugat automat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Fluxuri externe ale listei de blocare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Nume flux" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Selectarea fluxului" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Completare" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Jurnal Firewall" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Indicator" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Indicator neacceptat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Setări Generale" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Acordarea accesului la aplicația LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Prioritate ridicată" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Cea mai mare prioritate" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Căutare IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Căutare IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Suport IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Suport IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Creșteți numărul maxim de fișiere deschise, de exemplu, pentru a gestiona " +"numărul de fișiere divizate temporar în timpul încărcării seturilor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Informații" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Caractere nevalide" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Valori de intrare nevalide, nu se pot salva modificările." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Ultima Pornire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Cea mai mică prioritate" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Cea mai mică prioritate" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Limitați funcția uplink autoallow." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Lista interfețelor de rețea disponibile pentru a declanșa pornirea banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Enumeră elementele unui anumit set legat de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Număr de jurnale" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Limita de jurnal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Setări jurnal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Termeni de jurnal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Resetare hartă" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Hartă..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Maxim de fișiere deschise" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Informații NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Dispozitive de rețea" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nivel Bun" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Fără date de hartă GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Prioritate normală" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Numărul de încercări de descărcare în caz de eroare (nu este suportat de " +"uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Numărul de încercări de conectare eșuate ale aceluiași IP din jurnal înainte " +"de blocare." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Prezentare generală" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Analizează numai ultimul număr declarat de intrări în jurnal pentru " +"evenimente suspecte. Pentru a dezactiva monitorul de jurnal, setați-l la '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Jurnal de procesare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Profilul utilizat de 'msmtp' pentru mesajele electronice de notificare banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Primiți notificări prin e-mail la fiecare banIP rulat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Adresa destinatarului pentru e-mailurile de notificare BanIP; această " +"informație este necesară pentru a activa funcționalitatea e-mailurilor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Reîmprospătare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Director de rapoarte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Restricționați accesul la internet de la/către un număr mic de IP-uri " +"securizate." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Rezultat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Indicatoare de pornire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Informații despre cursă" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Salvați" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Salvare și reîncărcare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Salvare și repornire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Căutați un anumit IP în seturile legate de banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Selectați unul dintre utilitățile de descărcare preconfigurate." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Selectați dispozitivul (dispozitivele) de rețea WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Selectați interfața (interfețele) logică (logice) de rețea WAN IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Selectați interfața (e) logică de rețea WAN IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Adresa expeditorului pentru e-mailurile de notificare banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Setați" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Stabilirea politicii" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Set raportare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Setați Dimensiunea Divizată" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Setați detaliile" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Setați nivelul syslog pentru jurnalizarea NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Setări" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Interfața de declanșare a pornirii" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Stare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Oprire" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Subnet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Informații despre sistem" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Directorul țintă pentru fișierele de raportare referitoare la banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Directorul țintă pentru copiile de rezervă ale fluxurilor comprimate." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Lista de permise este prea mare, nu se pot salva modificările." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Lista de blocare este prea mare, nu se pot salva modificările." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" +"Prioritatea selectată va fi utilizată pentru procesarea în fundal a banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Aceasta este lista locală de permisiuni banIP care va permite anumite adrese " +"MAC, IP sau nume de domenius.
Vă rugăm să rețineți: se " +"adaugă doar exact un singur MAC/IPv4/IPv6 adresa sau numele de domeniu pe " +"linie. Sunt permise intervalele în notație CIDR și legăturile MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Aceasta este lista de blocare locală banIP care va împiedica anumite adrese " +"MAC, IP sau nume de domenii.
Vă rugăm să rețineți: " +"adăugați doar exact o singură adresă MAC/IPv4/IPv6 sau nume de domeniu pe " +"linie. Sunt permise intervale în notație CIDR și MAC/IP-bindings." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Înregistrarea timpului" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Subiect pentru e-mailurile de notificare banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Întârzierea declanșării" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Nu se poate analiza fișierul de flux personalizat!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Nu se poate analiza fișierul de flux implicit!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Nu se pot salva modificările: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Încărcați" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Încărcarea fișierului de flux personalizat a eșuat." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Jurnalizare Detaliată de Depanare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "alertă" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "critic" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "depanare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "ignoră" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "urgență" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "eroare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "informații" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lista locală de permise" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lista de blocare locală" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "memorie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "notificare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "performanță" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "respinge" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "atenție" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Suprascrieți opțiunile de descărcare preconfigurate pentru utilitarul de " +#~ "descărcare selectat." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Setați politica nft pentru seturile legate de banIP." + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Formatul protocolului/URL nu este acceptat" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Pentru a activa notificările prin e-mail, configurați pachetul \"msmtp\" " +#~ "și specificați o adresă de destinatar de e-mail validă." + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Versiune" + +#~ msgid "Elements" +#~ msgstr "Elemente" diff --git a/luci-app-secbox/po/ru/secbox.po b/luci-app-secbox/po/ru/secbox.po new file mode 100644 index 000000000..3166f19a1 --- /dev/null +++ b/luci-app-secbox/po/ru/secbox.po @@ -0,0 +1,1560 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-08-11 04:09+0000\n" +"Last-Translator: SnIPeRSnIPeR " +"\n" +"Language-Team: Russian \n" +"Language: ru\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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" +"X-Generator: Weblate 2026.9.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Пожалуйста, выберите (необязательно) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Выберите набор --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- по умолчанию --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "<Пробел><Маска подсети>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "<Синтаксис Suricata>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - обслуживание Африки и региона Индийского океана" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - обслуживание Азиатско-Тихоокеанского региона" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - обслуживание Канады и США" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Выбор ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "Автономные системы" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Активные устройства" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Активные подписки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Активный аплинк" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Ошибка входа в AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "Дополнительная задержка срабатывания правил banIP в секундах." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Расширенные настройки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Разрешить протокол/порты" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Разрешить переадресацию VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Разрешённые URL-адреса каналов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Только список разрешений" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Изменения в Allowlist были сохранены, перезагрузите BanIP, чтобы изменения " +"вступили в силу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Всегда разрешать протокол (TCP/UDP) с определёнными портами или диапазонами " +"портов в цепочке WAN-Input и WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Всегда разрешать определённые переадресации VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Всегда блокировать определённые переадресации VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Автоматически разрешать исходящие соединения" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Автоматический список разрешений" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Автоблокировка подсети" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Автоматический блок-лист" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Автоопределение" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Автоматическое добавление целых подсетей в список блокировок Set на основе " +"дополнительного запроса RDAP с подозрительным IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Автоматически добавлять домены и подозрительные IP-адреса в список " +"блокировок banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Автоматически добавлять обнаруженные домены и подозрительные IP-адреса в " +"локальный список блокировок banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Каталог резервных копий" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Базовый каталог" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Базовый рабочий каталог при обработке banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Блокировка переадресации VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Блокировать пакеты с поддельными IP-адресами источника во всех " +"поддерживаемых цепочках." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Фильтр(ы) блокировок" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Задать срок действия блок-листа" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Изменения в списке блокировок сохранены. Перезапустите banIP для применения " +"изменений." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Размер всплеска (в пакетах) для общего ограничения журнала NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Ядра CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Отмена" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Цепь" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Приоритет цепочки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Чтобы изменения на этой вкладке вступили в силу, необходима перезагрузка " +"службы banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Для вступления в силу изменений на этой вкладке требуется перезапуск службы " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Сбросить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Коллекция IP-адресов, основанная на номерах автономных систем (ASN)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Конфигурация пакета banIP для блокировки входящих и исходящих IP-адресов с " +"помощью именованных наборов nftables. Для получения дополнительной " +"информации ознакомьтесь с %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Кол-во" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Страны" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Выбор страны" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Редактор собственных списков" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Исключите дублирование IP-адресов во всех активных наборах и приведите в " +"порядок локальный список блокировок." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Дублирование IP-адресов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Описание" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Автоматическое обнаружение соответствующих сетевых устройств, интерфейсов, " +"подсетей, протоколов и утилит." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Отключить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" +"Не проверять SSL-сертификаты " +"серверов при загрузке." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Скачать" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Небезопасная загрузка" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Параметры загрузки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Повторные попытки загрузки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Утилита для загрузки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"Автоматически отбрасывайте пакеты или активно отклоняйте входящий трафик." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Уведомление по почте" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Профиль почты" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Адрес получателя" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Адрес отправителя" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Настройки почты" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Тема письма" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Разрешённые домены" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Блокируемые домены" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Количество элементов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Элементы (не более 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Поле не может быть пустым" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Включить BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Включить карту GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Включить счётчики NFT для элементов наборов и правил цепочек. Необходимо для " +"работы карты GeoIP и статистики пакетов в отчётах." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Включить удалённое ведение журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Активировать GeoIP-карту для подозрительных элементов Set. Для этого " +"необходимы внешние запросы для загрузки карт и геоданных." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Включить сервис banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Включите интерфейс cgi для получения удалённых событий." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Включите ведение подробного журнала отладки в случае ошибок обработки." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Включено" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Включает поддержку IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Включает поддержку IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Ошибка каталога" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Срок действия для автоматически добавленных блоклистов Задать участников." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Внешние разрешённые списки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Внешние источники списков блокировок" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Не удалось создать отчёт banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Подача завершена" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Сброс флага подачи" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Название канала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Выбор подписок" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Настройки каналов/сетов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Заполнить" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Журнал Firewall" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Флаг" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Флаг не поддерживается" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Основные настройки" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Предоставить доступ к LuCI-приложению banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Выше среднего" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Высокий" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-порог" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ICMP-Пороговое значение в пакетах в секунду для предотвращения WAN-DoS-атак. " +"Чтобы отключить эту защиту, установите значение '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Поиск IP-адресов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Поиск IP-адресов..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Сетевые интерфейсы IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Поддержка iPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Сетевые интерфейсы IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Поддержка IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Входящий" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Входящие и Исходящие" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Входящий и Исходящий Канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Политика блокировки входящего трафика (Inbound Block Policy)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Входящий канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Входящие (пакеты)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Увеличьте максимальное количество открытых файлов, например, чтобы " +"справиться с количеством временных разделённых файлов при загрузке наборов." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Информация" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Недопустимый формат URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Недопустимые символы" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Неверный формат срока действия (верный: 5m, 2h, 1d или 1h30m)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Некорректные значения, изменения не сохранены." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - обслуживание региона Латинской Америки и Карибского бассейна" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Последний запуск" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Наименьший" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Ниже среднего" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Ограничение используемых ядер CPU для banIP с целью экономии ОЗУ (по " +"умолчанию определяется автоматически)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Ограничить функцию автоматического разрешения для исходящего канала." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "Список доступных сетевых интерфейсов запускающих banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Перечислите элементы конкретного набора, связанного с запретом." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Настройки локальных каналов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Место для разбора лог-файла, например, через syslog-ng, для отключения " +"стандартного разбора через logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Предел всплеска журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Счётчик журналов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Логировать входящие" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Лимит журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Логировать исходящие" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Журнал предварительной маршрутизации" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Ограничение скорости журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Настройки журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Условия ведения журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Логировать подозрительные пакеты в цепочке LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Логировать подозрительные пакеты в цепочке PREROUTING." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Логировать подозрительные пакеты в цепях WAN-Input и WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Файл журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "Неудачный вход в LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Сброс карты" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Карта..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Максимальное количество открытых файлов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Информация NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Уровень журнала NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Сетевые устройства" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Ошибка сети" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Приоритет процесса" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Пока нет журналов, связанных с %s !" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Данные для карты GeoIP отсутствуют!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Конфигурация BanIP не найдена!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Обычный" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Сколько раз повторить загрузку параметров при ошибке." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Количество повторных попыток загрузки при возникновении ошибки (не " +"поддерживается uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Количество неудачных попыток входа с одного и того же IP в журнале перед " +"блокировкой." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Отключить дедупликацию для выбранных источников." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Исходящий" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Исходящий канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Исходящие (пакеты)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Игнорировать настройки фида по умолчанию и задействовать его для обработки " +"входящих (inbound) и исходящих (outbound) подключений." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Переопределить стандартную конфигурацию фида и применить его только к " +"входящей цепочке." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Переопределить стандартную конфигурацию фида и применить его только к " +"исходящей цепочке." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Переопределить стандартную конфигурацию фида и удалить существующие " +"ограничения портов/протоколов." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Переопределить предварительно настроенные параметры загрузки для выбранной " +"утилиты. Параметр вывода (например, «-o» для curl или «-O» для wget) должен " +"быть указан последним." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Обзор" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Разбор только последнего указанного количества записей журнала на предмет " +"подозрительных событий. Чтобы вообще отключить монитор журнала, установите " +"значение '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Порт / Протокол" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Обработка журнала" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Профиль, используемый 'msmtp' для электронной почты с уведомлением banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - обслуживание Европы, Ближнего Востока и Центральной Азии" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Скорость (в секунду) для общего ограничения журнала NFT, применяемого ко " +"всем журналируемым правилам. Установите «0», чтобы полностью отключить " +"ограничение скорости, например, при использовании ulogd или других " +"обработчиков журнала в пользовательском пространстве." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Получайте уведомления по электронной почте при каждом запуске banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Адрес получателя электронной почты для уведомлений banIP, эта информация " +"необходима для включения функции электронной почты." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Обновить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Региональный интернет-регистр" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Регулярные выражения для поиска подозрительных IP в системном журнале. По " +"умолчанию берётся последний IP в строке; чтобы брать первый (исходный), " +"начните шаблон с «first:» (например, nginx)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Удалённый токен" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Удалённое логирование событий" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Каталог для отчётов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Счётчики для отчётов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Ограничить доступ в Интернет для небольшого числа доверенных IP-адресов." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Результат" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Правило" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Флаги запуска" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Информация о запуске" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN-порог" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"SYN-Порог в пакетах в секунду для предотвращения WAN-DoS-атак. Чтобы " +"отключить эту защиту, установите значение '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Сохранить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Сохранить и перезагрузить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Сохранить и перезапустить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Поиск IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Выполняется поиск, пожалуйста, подождите..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Поиск указанного IP-адреса в наборах banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Время поиска истекло." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Выберите одну из предварительно настроенных утилит загрузки." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Выберите устройство (устройства) сети WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Выберите логический сетевой интерфейс (интерфейсы) WAN IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Выберите логический сетевой интерфейс (интерфейсы) WAN IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Адрес отправителя для электронных писем с уведомлением banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Набор" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Задать содержимое" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Задать содержимое..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Задать попытки загрузки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Настройка политики" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Настройка отчётов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Установить размер разделения" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Сведения о наборе" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Задаёт приоритет цепочки NFT в таблице banIP: чем меньше значение, тем выше " +"приоритет." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Задаёт политику NFT для наборов banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Установите уровень syslog для ведения журнала NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Настройки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Отобразить содержимое" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Показывать только элементы набора с просмотрами" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Разделить набор ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Разделить набор стран" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Разбивать загрузку внешнего набора после каждых n элементов для экономии " +"оперативной памяти, отключено по умолчанию." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Интерфейс(ы) для запуска" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Статус" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Остановить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Подсеть" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" +"Сводка по странам на основе данных регионального интернет-реестра (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Информация о системе" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Настройки таблиц/цепочек" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Целевой каталог для файлов ошибок banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Целевой каталог для файлов отчётов, связанных с banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Целевой каталог для сжатых резервных копий." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Список разрешений слишком велик, изменения не могут быть сохранены." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Список блокировок слишком велик, изменения не могут быть сохранены." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Выбранные ASN сохраняются в отдельных наборах." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Выбранные страны сохраняются в отдельных наборах." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Выбранный приоритет будет использоваться для фоновой обработки banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"Вывод syslog, предварительно отфильтрованный по сообщениям, связанным с: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Это локальный список разрешений banIP, который разрешает определённые MAC/IP-" +"адреса или доменные имена.
Обратите внимание: " +"добавляет только ровно один MAC/IPv4/IPv6-адрес или доменное имя на строку. " +"Разрешены диапазоны в нотации CIDR и MAC/IP-связи." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Это локальный блок-лист banIP, который запрещает определённые MAC-, IP-" +"адреса или доменные имена.
Обратите внимание: " +"добавляет только ровно один MAC/IPv4/IPv6-адрес или доменное имя на строку. " +"Допускаются диапазоны в нотации CIDR и MAC/IP-связи." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Отчёт показывает последнюю статистику наборов NFT. Нажмите «Обновить», чтобы " +"получить новый отчёт. Можно посмотреть содержимое наборов, найти " +"подозрительные IP или отобразить их на карте." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Временная метка" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Чтобы включить уведомления по электронной почте, настройте пакет 'msmtp' и " +"укажите действительный адрес получателя." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Токен для связи с интерфейсом cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Тема для электронной почты с уведомлением о banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Задержка запуска" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP-порог" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"UDP-Порог в пакетах в секунду для предотвращения WAN-DoS-атак. Чтобы " +"отключить эту защиту, установите значение '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Не удалось обработать информацию о работе banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Невозможно разобрать файл стран!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Не получилось прочитать ваш файл. Проверьте формат!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" +"Система не может прочитать основной файл с данными. Возможно, он повреждён " +"или имеет неверный формат!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Невозможно разобрать файл набора правил!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Не удалось сохранить изменения: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Загрузить" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Ошибка загрузки файла подписок." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Подробный журнал отладки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"С помощью этого редактора вы можете загрузить локальный файл " +"пользовательских списков или создать начальный файл (точно такой же, как " +"изначально поставляемый с пакетом). Файл находится по пути /etc/banip/" +"banip.custom.feeds. Вы можете редактировать этот файл, удалять записи, " +"добавлять новые или создавать локальную резервную копию. Чтобы вернуться к " +"версии из пакета, удалите содержимое пользовательского файла списков." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "оповещение" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "asterisk: недействительный аккаунт" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "автоматически добавлено в список разрешений" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "автоматически добавлено в список блокировок" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "заблокировано пакетов BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "заблокировано ICMP-флуд пакетов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "заблокировано ct-пакетов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "заблокировано недопустимых TCP-пакетов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "заблокировано SYN-флуд пакетов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "заблокировано UDP-флуд пакетов" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "крит" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "отладка" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "отбрасывать" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "dropbear: неудачная попытка входа" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "чрезв" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "ошибка" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "информация" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "локальный список разрешений" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "локальный список блокировок" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "память" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "уведомление" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "документация" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "openvpn: ошибка TLS" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "производительность" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "отклонять" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd: соединение закрыто" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "sshd: неудачная попытка входа" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "предупреждение" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Регулярные выражения для обнаружения подозрительных IP-адресов в " +#~ "системном журнале." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Отмена предварительно настроенных параметров загрузки для выбранной " +#~ "утилиты загрузки." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Путь к исполняемому скрипту." + +#~ msgid "Set Element Counter" +#~ msgstr "Задать счётчик элементов" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Устанавливает приоритет цепочки nft в таблице banIP, меньшие значения " +#~ "означают более высокий приоритет." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Установите политику nft для наборов, связанных с banIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx: подозрительный IP-адрес" + +#~ msgid " " +#~ msgstr " <Маска сети>" + +#~ msgid "Invalid expiry format" +#~ msgstr "Неверный формат срока действия" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Неподдерживаемый формат протокола/URL" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Чтобы включить уведомления по электронной почте, установите пакет 'msmtp' " +#~ "и укажите правильный адрес получателя электронной почты." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Не удалось обработать информацию о состоянии системы!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Конфигурация пакета banIP для запрета входящих и исходящих IP-адресов " +#~ "через именованные наборы nftables. Для получения дополнительной " +#~ "информации ознакомьтесь с онлайн-" +#~ "документацией" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", substring" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", concatinated" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "YOUR" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Версия" + +#~ msgid "logread not found on system." +#~ msgstr "logread не найден в системе." + +#~ msgid "banIP Log" +#~ msgstr "Журнал banIP" + +#~ msgid "banIP firewall Log" +#~ msgstr "Журнал брандмауэра banIP" + +#~ msgid "Elements" +#~ msgstr "Элементы" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "Карта GeoIP не включена!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Невозможно разобрать файл отчёта!" diff --git a/luci-app-secbox/po/sk/secbox.po b/luci-app-secbox/po/sk/secbox.po new file mode 100644 index 000000000..fb6d1e91f --- /dev/null +++ b/luci-app-secbox/po/sk/secbox.po @@ -0,0 +1,1327 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2025-05-30 20:28+0000\n" +"Last-Translator: Jan Vongrej \n" +"Language-Team: Slovak \n" +"Language: sk\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" +"X-Generator: Weblate 5.12-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Prosím vyberte(voliteľné) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Záložný priečinok" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Zrušiť" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Popis" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Nástroj na sťahovanie" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Zapnuté" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Prehľad" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Uložiť" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Nastavenia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" + +#~ msgid "Version" +#~ msgstr "Verzia" diff --git a/luci-app-secbox/po/sv/secbox.po b/luci-app-secbox/po/sv/secbox.po new file mode 100644 index 000000000..5c95e6b04 --- /dev/null +++ b/luci-app-secbox/po/sv/secbox.po @@ -0,0 +1,1422 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-08-13 09:16+0000\n" +"Last-Translator: Jonatan Nyberg \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.9.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Välj (valfritt) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Val av uppsättning --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- standard --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC – betjänar Afrika och regionen kring Indiska oceanen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC – betjänar Asien och Stillahavsområdet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN – betjänar Kanada och USA" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Val av ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN:er" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktiva enheter" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Aktiva flöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktiv upplänk" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Inloggningsfel i AdGuard Home" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Ytterligare utlösningsfördröjning i sekunder innan banIP-bearbetningen " +"börjar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "avancerade inställningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Tillåt protokoll/portar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Tillåt VLAN-vidarebefordran" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URL:er till tillåtelselisteflöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Endast tillåtelselista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Ändringarna i tillåtelselistan har sparats. Läs om banIP för att ändringarna " +"ska börja gälla." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Tillåt alltid ett protokoll (TCP/UDP) med vissa portar eller portintervall i " +"kedjorna WAN-Input och WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Tillåt alltid vissa VLAN-vidarebefordringar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Blockera alltid vissa VLAN-vidarebefordringar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Tillåt upplänk automatiskt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Automatisk tillåtelselista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Blockera undernät automatiskt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Automatisk blockeringslista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Automatisk detektering" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Lägg automatiskt till hela undernät i blockeringslistans uppsättning baserat " +"på en ytterligare RDAP-förfrågan för den misstänkta IP-adressen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Lägg automatiskt till upplösta domäner och misstänkta IP-adresser i den " +"lokala banIP-blockeringslistan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Lägg automatiskt till upplösta domäner och upplänks-IP-adresser i den lokala " +"banIP-tillåtelselistan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Säkerhetskopieringskatalog" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Baskatalog" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Grundläggande arbetskatalog under banIP-bearbetningen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Blockera VLAN-vidarebefordran" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Blockera paket med förfalskade käll-IP-adresser i alla kedjor som stöds." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Blockeringslisteflöde" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Giltighetstid för blockeringslistans uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Ändringarna i blockeringslistan har sparats. Läs om banIP för att " +"ändringarna ska börja gälla." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Burststorlek i paket för den gemensamma NFT-logggränsen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU-kärnor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Avbryt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Kedja" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Kedjeprioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Ändringar på den här fliken kräver att banIP-tjänsten läses om för att börja " +"gälla." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Ändringar på den här fliken kräver att banIP-tjänsten startas om för att " +"börja gälla." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Rensa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Samling av IP-adresser baserad på autonoma systemnummer (ASN)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Konfiguration av banIP-paketet för att blockera inkommande och utgående IP-" +"adresser via namngivna nftables-uppsättningar. Mer information finns i %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Antal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Länder" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Val av länder" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Redigerare för anpassade flöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Ta bort dubbletter av IP-adresser i alla aktiva uppsättningar och rensa den " +"lokala blockeringslistan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Ta bort IP-dubbletter" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Beskrivning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Upptäck automatiskt relevanta nätverksenheter, gränssnitt, undernät, " +"protokoll och verktyg." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Inaktivera" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Kontrollera inte serverns SSL-certifikat under hämtning." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Hämta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Osäker hämtning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Hämtningsparametrar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Nya hämtningsförsök" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Hämtningsverktyg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "Släpp paket utan svar eller avvisa inkommande trafik aktivt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "E-postavisering" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-postprofil" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Mottagarens e-postadress" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Avsändaradress för e-post" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "E-postinställningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-postämne" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Redigera tillåtelselista" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Redigera blockeringslista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Antal element" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Element (högst 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Tomt fält är inte tillåtet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Aktivera BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Aktivera GeoIP-karta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Aktivera NFT-räknare för uppsättningselement och kedjeregler. Krävs för " +"GeoIP-kartan och paketstatistiken i rapporteringen för uppsättningar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Aktivera fjärrloggning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Aktivera en GeoIP-karta med misstänkta uppsättningselement. Detta kräver " +"externa förfrågningar för att hämta kartplattor och geolokaliseringsdata." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Aktivera banIP-tjänsten." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Aktivera CGI-gränssnittet för att ta emot fjärrloggningshändelser." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Aktivera utförlig felsökningsloggning vid bearbetningsfel." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Aktiverad" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Aktiverar IPv4-stöd." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Aktiverar IPv6-stöd." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Felkatalog" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Giltighetstid för automatiskt tillagda medlemmar i blockeringslistans " +"uppsättning." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Externa tillåtelselisteflöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Externa blockeringslisteflöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Det gick inte att generera en banIP-rapport!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Flöde slutfört" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Återställ flödesflaggor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Flödesnamn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Val av flöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Inställningar för flöden/uppsättningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Fyll i" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Brandväggslogg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Flagga" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Flaggan stöds inte" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Allmänna inställningar" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Ge LuCI-programmet banIP åtkomst" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Hög prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Högsta prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-tröskel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ICMP-tröskel i paket per sekund för att förhindra WAN-DoS-attacker. Ange ”0” " +"för att inaktivera skyddet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP-sökning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Sök IP-adress…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4-nätverksgränssnitt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4-stöd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6-nätverksgränssnitt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6-stöd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Inkommande" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Inkommande och utgående" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Flöde för inkommande och utgående" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Blockeringspolicy för inkommande trafik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Flöde för inkommande trafik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Inkommande (paket)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Öka det högsta antalet öppna filer, t.ex. för att hantera mängden " +"tillfälliga delfiler när uppsättningarna läses in." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Information" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Ogiltigt URL-format" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Ogiltiga tecken" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Ogiltigt format för giltighetstid, t.ex. 5m, 2h, 1d eller 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Ogiltiga inmatningsvärden. Det gick inte att spara ändringarna." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC – betjänar Latinamerika och Karibien" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Kördes senast" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Lägsta prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Lägre prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Begränsa antalet CPU-kärnor som banIP använder för att spara RAM-minne. " +"Identifieras automatiskt som standard." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Begränsa funktionen som automatiskt tillåter upplänken." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" +"Lista över tillgängliga nätverksgränssnitt som kan utlösa start av banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Lista elementen i en viss banIP-relaterad uppsättning." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Inställningar för lokala flöden" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Plats för tolkning av loggfilen, t.ex. via syslog-ng, för att inaktivera " +"standardtolkningen via logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Burstgräns för logg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Loggantal" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Logga inkommande" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Logggräns" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Logga utgående" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Logga Prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Hastighetsgräns för loggning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Logginställningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Loggtermer" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Logga misstänkta paket i kedjan LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Logga misstänkta paket i kedjan Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Logga misstänkta paket i kedjorna WAN-Input och WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Loggfilens plats" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "Misslyckad LuCI-inloggning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Återställ karta" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Karta..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Högsta antal öppna filer" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT-information" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "NFT-loggnivå" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Nätverksenheter" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Nätverksfel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nice-nivå" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Inga loggar relaterade till %s ännu!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Inga GeoIP-kartdata!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Ingen banIP-konfiguration hittades!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normal prioritet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Antal försök att läsa in uppsättningen vid fel." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "Antal hämtningsförsök vid fel (stöds inte av uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Antal misslyckade inloggningsförsök från samma IP-adress i loggen innan den " +"blockeras." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Undanta vissa flöden från borttagningen av dubbletter." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Utgående" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Flöde för utgående trafik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Utgående (paket)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Åsidosätt standardkonfigurationen för flödet och tillämpa det på både " +"inkommande och utgående kedja." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Åsidosätt standardkonfigurationen för flödet och tillämpa det endast på den " +"inkommande kedjan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Åsidosätt standardkonfigurationen för flödet och tillämpa det endast på den " +"utgående kedjan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Åsidosätt standardkonfigurationen för flödet och ta bort befintliga port-/" +"protokollbegränsningar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Åsidosätt de förkonfigurerade hämtningsalternativen för det valda " +"hämtningsverktyget. Utdataflaggan, t.ex. ”-o” för curl eller ”-O” för wget, " +"måste vara den sista parametern." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Överblick" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Tolka endast det angivna antalet senaste loggposter för misstänkta " +"händelser. Ange ”0” för att inaktivera loggövervakningen helt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Port / Protokoll" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Bearbetningslogg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Profil som används av ”msmtp” för e-postaviseringar från banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE – betjänar Europa, Mellanöstern och Centralasien" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Hastighet (per sekund) för den gemensamma NFT-logggränsen, som tillämpas " +"globalt på alla loggade regler. Ange ”0” för att inaktivera " +"hastighetsbegränsningen helt, t.ex. när ulogd eller andra logghanterare i " +"användarrymden används." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Ta emot e-postaviseringar vid varje banIP-körning." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Mottagaradress för e-postaviseringar från banIP. Uppgiften krävs för att " +"aktivera e-postfunktionen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Uppdatera" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Regionalt internetregister" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Reguljära uttryck för att upptäcka misstänkta IP-adresser i systemloggen. " +"Den sista IP-adressen på varje rad används; lägg till prefixet ”first:” för " +"format där källan står först, t.ex. nginx." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Fjärrtoken" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Fjärrloggningshändelse" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Rapportkatalog" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Rapporteringsräknare" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Begränsa internetåtkomsten från/till ett litet antal betrodda IP-adresser." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Resultat" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Regel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Körflaggor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Körningsinformation" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN-tröskel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"SYN-tröskel i paket per sekund för att förhindra WAN-DoS-attacker. Ange ”0” " +"för att inaktivera skyddet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Spara" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Spara och läs om" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Spara och starta om" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Sök IP-adress" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Sökningen pågår. Vänta…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Sök efter en viss IP-adress i de banIP-relaterade uppsättningarna." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Tidsgränsen för sökningen överskreds." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Välj ett av de förkonfigurerade hämtningsverktygen." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Välj WAN-nätverksenhet(er)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Välj logiska WAN-IPv4-nätverksgränssnitt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Välj logiska WAN-IPv6-nätverksgränssnitt." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Avsändaradress för e-postaviseringar från banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Innehåll i uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Innehåll i uppsättning…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Nya försök att läsa in uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Policy för uppsättning" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Rapportering för uppsättningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Delningsstorlek för uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Detaljer för uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Ange NFT-kedjans prioritet i banIP-tabellen. Lägre värden innebär högre " +"prioritet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Ange NFT-policyn för banIP-relaterade uppsättningar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Ange syslog-nivån för NFT-loggning." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Inställningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Visa innehåll" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Visa endast uppsättningselement med träffar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Dela ASN-uppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Dela landsuppsättning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Dela inläsningen av externa uppsättningar efter var n:e medlem för att spara " +"RAM-minne. Inaktiverat som standard." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Gränssnitt som utlöser start" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Status" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Stoppa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Undernät" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "Sammanfattning av länder baserad på regionala internetregister (RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Systeminformation" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Tabell-/kedjeinställningar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Målkatalog för banIP-relaterade felfiler." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Målkatalog för banIP-relaterade rapportfiler." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Målkatalog för komprimerade säkerhetskopior av flöden." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Tillåtelselistan är för stor. Det gick inte att spara ändringarna." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Blockeringslistan är för stor. Det gick inte att spara ändringarna." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "De valda ASN-numren lagras i separata uppsättningar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "De valda länderna lagras i separata uppsättningar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Den valda prioriteten används för banIP:s bakgrundsbearbetning." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "Syslog-utdata, förfiltrerad för meddelanden relaterade till: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Detta är den lokala banIP-tillåtelselistan som tillåter vissa MAC-adresser, " +"IP-adresser eller domännamn.
Observera: lägg endast " +"till exakt en MAC-/IPv4-/IPv6-adress eller ett domännamn per rad. Intervall " +"i CIDR-notation och MAC/IP-bindningar är tillåtna." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Detta är den lokala banIP-blockeringslistan som blockerar vissa MAC-" +"adresser, IP-adresser eller domännamn.
Observera: lägg " +"endast till exakt en MAC-/IPv4-/IPv6-adress eller ett domännamn per rad. " +"Intervall i CIDR-notation och MAC/IP-bindningar är tillåtna." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Rapporten visar den senaste statistiken för NFT-uppsättningar. Tryck på " +"knappen ”Uppdatera” för att skapa en ny rapport. Du kan också visa " +"innehållet i enskilda uppsättningar, söka efter misstänkta IP-adresser och " +"visa dem på en karta." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Tidsstämpel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Konfigurera paketet ”msmtp” och ange en giltig e-postadress för mottagaren " +"för att aktivera e-postaviseringar." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Token för kommunikation med CGI-gränssnittet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Ämne för e-postaviseringar från banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Utlösningsfördröjning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP-tröskel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"UDP-tröskel i paket per sekund för att förhindra WAN-DoS-attacker. Ange ”0” " +"för att inaktivera skyddet." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Det gick inte att tolka körningsinformationen för banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Det gick inte att tolka landsfilen!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Det gick inte att tolka den anpassade flödesfilen!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Det gick inte att tolka standardflödesfilen!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Det gick inte att tolka regeluppsättningsfilen!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Det gick inte att spara ändringarna: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Ladda upp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Uppladdningen av den anpassade flödesfilen misslyckades." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Utförlig felsökningsloggning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Med den här redigeraren kan du ladda upp en lokal anpassad flödesfil eller " +"skapa en ursprunglig fil (en 1:1-kopia av versionen som följer med paketet). " +"Filen finns i ”/etc/banip/banip.custom.feeds”. Du kan sedan redigera filen, " +"ta bort poster, lägga till nya eller skapa en lokal säkerhetskopia. Töm den " +"anpassade flödesfilen för att återgå till underhållarnas version." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "varning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "ogiltigt Asterisk-konto" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "automatiskt tillagda IP-adresser i tillåtelselistan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "automatiskt tillagda IP-adresser i blockeringslistan" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "blockerade BCP38-paket" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "blockerade ICMP-flood-paket" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "blockerade ogiltiga CT-paket" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "blockerade ogiltiga TCP-paket" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "blockerade SYN-flood-paket" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "blockerade UDP-flood-paket" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "kritisk" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "felsökning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "släpp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "misslyckad Dropbear-inloggning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "nödläge" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "fel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "lokal tillåtelselista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "lokal blockeringslista" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "minne" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "meddelande" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "onlinedokumentation" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "OpenVPN TLS-fel" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "prestanda" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "avvisa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd stängde anslutningen" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "misslyckad sshd-inloggning" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "varning" + +#~ msgid "Version" +#~ msgstr "Version" + +#~ msgid "Elements" +#~ msgstr "Element" diff --git a/luci-app-secbox/po/sw/secbox.po b/luci-app-secbox/po/sw/secbox.po new file mode 100644 index 000000000..839cb16b5 --- /dev/null +++ b/luci-app-secbox/po/sw/secbox.po @@ -0,0 +1,1324 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2021-08-13 20:25+0000\n" +"Last-Translator: Oyamo Brian \n" +"Language-Team: Swahili \n" +"Language: sw\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.8-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Vifaa Vinavyotumika" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/ta/secbox.po b/luci-app-secbox/po/ta/secbox.po new file mode 100644 index 000000000..3d5029f76 --- /dev/null +++ b/luci-app-secbox/po/ta/secbox.po @@ -0,0 +1,1494 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2025-07-24 15:20+0000\n" +"Last-Translator: தமிழ்நேரம் \n" +"Language-Team: Tamil \n" +"Language: ta\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.13-dev\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-தயவுசெய்து தேர்வு செய்ய (விரும்பினால்)-" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-தேர்வை அமைக்கவும்-" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-இயல்புநிலை-" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "அஃப்ரினிக் - ஆப்பிரிக்கா மற்றும் இந்தியப் பெருங்கடல் பிராந்தியத்திற்கு பணி செய்கிறது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - ஆசியா பசிபிக் பிராந்தியத்திற்கு பணி செய்கிறது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "அரின் - கனடா மற்றும் அமெரிக்காவிற்கு பணி செய்கிறது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN தேர்வு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "முளை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "செயலில் உள்ள சாதனங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "செயலில் உள்ள ஊட்டங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "செயலில் உள்ள அப்லிங்க்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Adguardhome உள்நுழைவு பிழை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "BANIP செயலாக்கம் தொடங்குவதற்கு முன் வினாடிகளில் கூடுதல் தூண்டுதல் நேரந்தவறுகை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "மேம்பட்ட அமைப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "நெறிமுறை/துறைமுகங்களை அனுமதிக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "VLAN முன்னோக்கி அனுமதிக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "AllowList ஊட்ட முகவரி கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "இசைவு பட்டியல் மட்டுமே" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"இசைவு பட்டியல் மாற்றங்கள் சேமிக்கப்பட்டுள்ளன, மாற்றங்கள் நடைமுறைக்கு வரும் BANIP ஐ மீண்டும் " +"ஏற்றவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"WAN-INPUT மற்றும் WAN- முன்னோக்கி சங்கிலியில் சில துறைமுகங்கள் அல்லது துறைமுக " +"வரம்புகளுடன் எப்போதும் ஒரு நெறிமுறையை (TCP/UDP) அனுமதிக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "எப்போதும் சில VLAN முன்னோக்குகளை அனுமதிக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "எப்போதும் சில VLAN முன்னோக்குகளைத் தடுக்கும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "ஆட்டோ இசைவு அப்லிங்க்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "ஆட்டோ இசைவு பட்டியல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "ஆட்டோ பிளாக் சப்நெட்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "ஆட்டோ பிளாக்லிச்ட்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "ஆட்டோ கண்டறிதல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"சந்தேகத்திற்கிடமான ஐபியுடன் கூடுதல் RDAP கோரிக்கையின் அடிப்படையில் பிளாக்லிச்ட் தொகுப்பில் " +"முழு சப்நெட்டுகளையும் தானாக சேர்க்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"உள்ளக BANIP BLOCKLIST இல் தீர்க்கப்பட்ட களங்களையும் சந்தேகத்திற்கிடமான ஐபிக்களையும் " +"தானாகவே சேர்க்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"தானாகவே தீர்க்கப்பட்ட களங்களைச் சேர்த்து உள்ளக BANIP AllowList இல் அப்லிங்க் ஐபிக்களைச் " +"சேர்க்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "காப்பு அடைவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "அடிப்படை அடைவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "BANIP செயலாக்கம் போது அடிப்படை வேலை அடைவு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "தொகுதி VLAN முன்னோக்கி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "பிளாக்லிச்ட் ஊட்டம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "பிளாக்லிச்ட் செட் காலாவதி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"பிளாக்லிச்ட் மாற்றங்கள் சேமிக்கப்பட்டுள்ளன, மாற்றங்கள் நடைமுறைக்கு வரும் BANIP ஐ மீண்டும் " +"ஏற்றவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "சிபியு கோர்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "ரத்துசெய்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "சங்கிலி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "சங்கிலி முன்னுரிமை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "இந்த தாவலில் ஏற்படும் மாற்றங்கள் நடைமுறைக்கு வர Banip பணி மறுஏற்றம் தேவை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "இந்த தாவலில் ஏற்படும் மாற்றங்கள் நடைமுறைக்கு வர Banip பணி மறுதொடக்கம் தேவை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "துடை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "தன்னாட்சி கணினி எண்களின் அடிப்படையில் ஐபி முகவரிகளின் சேகரிப்பு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "எண்ணுங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "நாடுகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "நாட்டின் தேர்வு" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "தனிப்பயன் தீவன ஆசிரியர்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"அனைத்து செயலில் உள்ள செட்களிலும் ஐபி முகவரிகளைக் குறைத்து, உள்ளக தடுப்புப்பட்டியலை " +"நேர்த்தியாக மாற்றவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "ஐ.பி.எச்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "விவரம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"தொடர்புடைய பிணைய சாதனங்கள், இடைமுகங்கள், சப்நெட்டுகள், நெறிமுறைகள் மற்றும் பயன்பாடுகளை " +"தானாக கண்டறியவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "முடக்கு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "பதிவிறக்கத்தின் போது SSL சேவையக சான்றிதழ்களை சரிபார்க்க வேண்டாம்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "பதிவிறக்கம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "பாதுகாப்பற்ற பதிவிறக்க" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "அளவுருக்களைப் பதிவிறக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "மறுபயன்பாடுகளைப் பதிவிறக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "பயன்பாட்டைப் பதிவிறக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" +"பாக்கெட்டுகளை அமைதியாக கைவிடுங்கள் அல்லது உள்வரும் போக்குவரத்தை தீவிரமாக நிராகரிக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "மின்னஞ்சல் அறிவிப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "மின்னஞ்சல் சுயவிவரம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "மின்னஞ்சல் ரிசீவர் முகவரி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "மின்னஞ்சல் அனுப்புநர் முகவரி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "மின்னஞ்சல் அமைப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "மின்னஞ்சல் தலைப்பு" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "AllowList ஐத் திருத்து" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "பிளாக்லிச்ட்டைத் திருத்து" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "உறுப்பு எண்ணிக்கை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "கூறுகள் (மிகுதியாக 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "வெற்று புலம் அனுமதிக்கப்படவில்லை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "GEOIP வரைபடத்தை இயக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "தொலைநிலை பதிவை இயக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"சந்தேகத்திற்கிடமான செட் கூறுகளுடன் உலகம் வரைபடத்தை இயக்கவும். வரைபட ஓடுகள் மற்றும் " +"புவிஇருப்பிட தரவைப் பெற இதற்கு வெளிப்புற கோரிக்கைகள் தேவை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "BANIP சேவையை இயக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "தொலைநிலை பதிவு நிகழ்வுகளைப் பெற சிசிஐ இடைமுகத்தை இயக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "செயலாக்க பிழைகள் ஏற்பட்டால் வாய்மொழி பிழைத்திருத்த பதிவை இயக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "இயக்கப்பட்டது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "ஐபிவி 4 ஆதரவை செயல்படுத்துகிறது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "IPv6 ஆதரவை செயல்படுத்துகிறது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "பிழை அடைவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "ஆட்டோ சேர்க்கப்பட்ட பிளாக்லிச்ட் செட் உறுப்பினர்களுக்கான காலாவதி நேரம்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "வெளிப்புற இசைவு பட்டியல் ஊட்டங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "வெளிப்புற பிளாக்லிச்ட் ஊட்டங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "முழுமையான ஊட்டம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "தீவனக் கொடி மீட்டமைப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "உணவுப் பெயர்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "உணவுத் தேர்வு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "அமைப்புகளை உணவளிக்கவும்/அமைக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "நிரப்பவும்" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "ஃபயர்வால் பதிவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "கொடி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "கொடி ஆதரிக்கப்படவில்லை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "பொது அமைப்புகள்" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "லூசி ஆப் பானிக்குக்கு அணுகல் வழங்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "அதிக முன்னுரிமை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "மிகஅதிக முன்னுரிமை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-threshold" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"WAN-DOS தாக்குதல்களைத் தடுக்க நொடிக்கு பாக்கெட்டுகளில் ICMP-TRESHOLD. இந்த பாதுகாப்பை " +"முடக்க அதை '0' ஆக அமைத்தது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "ஐபி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "ஐபி தேடல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "ஐபி தேடல் ..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "ஐபிவி 4 பிணைய இடைமுகங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "ஐபிவி 4 உதவி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6 பிணைய இடைமுகங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "ஐபிவி 6 உதவி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "உள்நோக்கி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "உள்வரும் & வெளிச்செல்லும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "உள்வரும் மற்றும் வெளிச்செல்லும் தீவனம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "உள்வரும் தொகுதி கொள்கை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "உள்வரும் ஊட்டம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "உள்வரும்   (பாக்கெட்டுகள்)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"திறந்த கோப்புகளின் அதிகபட்ச எண்ணிக்கையை அதிகரிக்கவும், எ.கா. தொகுப்புகளை ஏற்றும்போது " +"தற்காலிக பிளவு கோப்புகளின் அளவைக் கையாள." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "தகவல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "தவறான எழுத்துக்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "தவறான உள்ளீட்டு மதிப்புகள், மாற்றங்களைச் சேமிக்க முடியவில்லை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "லாக்னிக் - லத்தீன் அமெரிக்க மற்றும் கரீபியன் பிராந்தியத்திற்கு பணி செய்தல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "கடைசி ரன்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "மிககுறைந்த முன்னுரிமை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "குறைந்த முன்னுரிமை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"இயல்புநிலையாக தன்னியக்கமாக்கப்பட்ட ரேம் சேமிக்க BANIP ஆல் பயன்படுத்தப்படும் சிபியு கோர்களை " +"கட்டுப்படுத்துங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "அப்லிங்க் ஆட்டோஅல்லோ செயல்பாட்டைக் கட்டுப்படுத்துங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "BANIP தொடக்கத்தைத் தூண்டுவதற்கு கிடைக்கக்கூடிய பிணைய இடைமுகங்களின் பட்டியல்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "ஒரு குறிப்பிட்ட BANIP தொடர்பான தொகுப்பின் கூறுகளை பட்டியலிடுங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "உள்ளக தீவன அமைப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"பதிவு கோப்பை பாகுபடுத்துவதற்கான இடம், எ.கா. சிச்லாக்-என்சி வழியாக, லோகிரெட் வழியாக " +"நிலையான பாகுபடுத்தலை செயலிழக்கச் செய்ய." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "பதிவு எண்ணிக்கை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "உள்வரும் உள்நுழைக" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "பதிவு வரம்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "வெளியில் உள்நுழைக" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "பதிவு முன்கூட்டியே" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "பதிவு அமைப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "பதிவு விதிமுறைகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "லேன்-ஃபார்வர்ட் சங்கிலியில் சந்தேகத்திற்கிடமான பாக்கெட்டுகளை பதிவு செய்யுங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "சந்தேகத்திற்கிடமான பாக்கெட்டுகளை முன்கூட்டியே சங்கிலியில் பதிவு செய்யுங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" +"WAN-INPUT மற்றும் WAN-FORWARD சங்கிலியில் சந்தேகத்திற்கிடமான பாக்கெட்டுகளை பதிவு " +"செய்யுங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "லாக்ஃபைல் இடம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "லூசி உள்நுழைவு தோல்வியுற்றார்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "வரைபட மீட்டமைப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "வரைபடம் ..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "அதிகபட்ச திறந்த கோப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT செய்தி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "NFT பதிவு நிலை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "பிணைய சாதனங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "நல்ல நிலை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "சியோப் வரைபட தரவு இல்லை!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "சாதாரண முன்னுரிமை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "பிழை ஏற்பட்டால் செட் சுமை முயற்சிகளின் எண்ணிக்கை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"பிழை ஏற்பட்டால் பதிவிறக்க முயற்சிகளின் எண்ணிக்கை (uclient-fetch ஆல் ஆதரிக்கப்படவில்லை)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "தடுப்பதற்கு முன் பதிவில் அதே ஐபியின் தோல்வியுற்ற உள்நுழைவு முயற்சிகளின் எண்ணிக்கை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "கழித்தல் செயல்முறையிலிருந்து குறிப்பிட்ட ஊட்டங்களைத் தேர்வுசெய்க." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "வெளிநொக்கி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "வெளிச்செல்லும் ஊட்டம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "வெளிச்செல்லும்   (பாக்கெட்டுகள்)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"இயல்புநிலை தீவன உள்ளமைவை மீறி, உள்வரும் மற்றும் வெளிச்செல்லும் சங்கிலிக்கு ஊட்டத்தைப் " +"பயன்படுத்துங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"இயல்புநிலை தீவன உள்ளமைவை மீறி, உள்வரும் சங்கிலிக்கு மட்டுமே ஊட்டத்தைப் பயன்படுத்துங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"இயல்புநிலை தீவன உள்ளமைவை மீறி, வெளிச்செல்லும் சங்கிலிக்கு மட்டுமே ஊட்டத்தைப் பயன்படுத்துங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "இயல்புநிலை தீவன உள்ளமைவை மீறி, ஏற்கனவே உள்ள போர்ட்/நெறிமுறை வரம்புகளை அகற்றவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "கண்ணோட்டம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"சந்தேகத்திற்கிடமான நிகழ்வுகளுக்கு கடைசியாக பதிவுசெய்யப்பட்ட பதிவு உள்ளீடுகளை மட்டுமே " +"அலசுங்கள். பதிவு மானிட்டரை முடக்க அதை '0' என அமைக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "துறைமுகம்  /  நெறிமுறை" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "பதிவு செயலாக்க" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "BANIP அறிவிப்பு மின்னஞ்சல்களுக்கு 'MSMTP' பயன்படுத்தும் சுயவிவரம்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "பழுத்த - ஐரோப்பா, நடு கிழக்கு மற்றும் நடு ஆசியா பணி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "ஒவ்வொரு BANIP ஓட்டத்திலும் மின்னஞ்சல் அறிவிப்புகளைப் பெறுங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"BANIP அறிவிப்பு மின்னஞ்சல்களுக்கான ரிசீவர் முகவரி, மின்னஞ்சல் செயல்பாட்டை இயக்க இந்த செய்தி " +"தேவைப்படுகிறது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "புதுப்பிப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "பிராந்திய இணைய பதிவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "தொலைநிலை கிள்ளாக்கு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "அறிக்கை கோப்பகம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "இணைய அணுகலை/குறைந்த எண்ணிக்கையிலான பாதுகாப்பான ஐபிக்களுக்கு கட்டுப்படுத்தவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "முடிவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "கொடிகளை இயக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "தகவல்களை இயக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "ஒத்திசைவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"WAN-DOS தாக்குதல்களைத் தடுக்க நொடிக்கு பாக்கெட்டுகளில் ஒத்திசைவு. இந்த பாதுகாப்பை முடக்க " +"அதை '0' ஆக அமைத்தது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "சேமி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "சேமித்து மீண்டும் ஏற்றவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "சேமிக்கவும் மறுதொடக்கம் செய்யவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "ஐபி தேடுங்கள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "ஒரு குறிப்பிட்ட ஐபிக்கு BANIP தொடர்பான தொகுப்புகளைத் தேடுங்கள்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "முன்பே கட்டமைக்கப்பட்ட பதிவிறக்க பயன்பாடுகளில் ஒன்றைத் தேர்ந்தெடுக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "வான் பிணையம் சாதனம் (களை) தேர்ந்தெடுக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "தருக்க வான் IPV4 பிணையம் இடைமுகம் (கள்) என்பதைத் தேர்ந்தெடுக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "தருக்க வான் IPv6 பிணையம் இடைமுகம் (கள்) என்பதைத் தேர்ந்தெடுக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "BANIP அறிவிப்பு மின்னஞ்சல்களுக்கான அனுப்புநர் முகவரி." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "கணம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "உள்ளடக்கத்தை அமைக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "உள்ளடக்கத்தை அமைக்கவும் ..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "சுமை முயற்சிகளை அமைக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "கொள்கையை அமைக்கவும்" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "அறிக்கையிடலை அமைக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "பிளவு அளவை அமைக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "விவரங்களை அமைக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "NFT பதிவு செய்ய சிச்லாக் அளவை அமைக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "அமைப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "உள்ளடக்கத்தைக் காட்டு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "வெற்றிகளுடன் கூடிய கூறுகளை மட்டும் காட்டு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "பிளவு ASN கணம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "பிளவு நாடு தொகுப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"இயல்புநிலையாக முடக்கப்பட்ட ரேம் சேமிக்க ஒவ்வொரு N உறுப்பினர்களுக்கும் பிறகு வெளிப்புற " +"தொகுப்பு ஏற்றுதல்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "தொடக்க தூண்டுதல் இடைமுகம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "நிலை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "நிறுத்து" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "சப்நெட்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "பிராந்திய இணைய பதிவேட்டில் (ஆர்.ஐ.ஆர்) அடிப்படையிலான நாடுகளின் சுருக்கம்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "கணினி செய்தி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "அட்டவணை/சங்கிலி அமைப்புகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "BANIP தொடர்பான பிழை கோப்புகளுக்கான இலக்கு அடைவு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "BANIP தொடர்பான அறிக்கை கோப்புகளுக்கான இலக்கு அடைவு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "சுருக்கப்பட்ட ஊட்ட காப்புப்பிரதிகளுக்கான இலக்கு அடைவு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "இசைவு பட்டியல் மிகப் பெரியது, மாற்றங்களைச் சேமிக்க முடியவில்லை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "பிளாக்லிச்ட் மிகப் பெரியது, மாற்றங்களைச் சேமிக்க முடியவில்லை." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "தேர்ந்தெடுக்கப்பட்ட ASN கள் தனி தொகுப்புகளில் சேமிக்கப்படுகின்றன." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "தேர்ந்தெடுக்கப்பட்ட நாடுகள் தனி தொகுப்புகளில் சேமிக்கப்படுகின்றன." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "BANIP பின்னணி செயலாக்கத்திற்கு தேர்ந்தெடுக்கப்பட்ட முன்னுரிமை பயன்படுத்தப்படும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"இது சில மேக்-, ஐபி-முகவரிகளை அல்லது டொமைன் பெயர்களை அனுமதிக்கும் உள்ளூர் banIP " +"அனுமதிப் பட்டியல்.
தயவுசெய்து கவனிக்கவும்: ஒரு வரிக்குச் " +"சரியாக ஒரு மேக்/ஐபிப4/ஐபிப6 முகவரி அல்லது டொமைன் பெயரை மட்டும் சேர்க்கவும். CIDR " +"குறியீடு மற்றும் மேக்/ஐபி-பிணைப்புகளில் வரம்புகள் அனுமதிக்கப்படுகின்றன." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"இது சில MAC-, IP-முகவரிகளை அல்லது டொமைன் பெயர்களைத் தடுக்கும் உள்ளூர் banIP தடுப்புப் " +"பட்டியல்.
தயவுசெய்து கவனிக்கவும்: ஒரு வரிக்கு சரியாக ஒரு MAC/" +"IPv4/IPv6 முகவரி அல்லது டொமைன் பெயரை மட்டும் சேர்க்கவும். CIDR குறியீடு மற்றும் MAC/IP-" +"பிணைப்புகளில் வரம்புகள் அனுமதிக்கப்படுகின்றன." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"இந்த அறிக்கை அண்மைக் கால NFT தொகுப்பு புள்ளிவிவரங்களைக் காட்டுகிறது, புதியதைப் பெற " +"'புதுப்பிப்பு' பொத்தானை அழுத்தவும். நீங்கள் தொகுப்புகளின் குறிப்பிட்ட உள்ளடக்கத்தையும் " +"காண்பிக்கலாம், சந்தேகத்திற்கிடமான ஐபிக்களைத் தேடலாம், இறுதியாக, இந்த ஐபிக்களையும் ஒரு " +"வரைபடத்தில் காட்டலாம்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "நேர முத்திரை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "சிசிஐ இடைமுகத்துடன் தொடர்பு கொள்ள கிள்ளாக்கு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "BANIP அறிவிப்பு மின்னஞ்சல்களுக்கான தலைப்பு." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "தூண்டுதல் நேரந்தவறுகை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "Udp-threshold" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"WAN-DOS தாக்குதல்களைத் தடுக்க நொடிக்கு பாக்கெட்டுகளில் யுடிபி-வாசல். இந்த பாதுகாப்பை " +"முடக்க அதை '0' ஆக அமைத்தது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLV4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLV6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "நாடுகளின் கோப்பை அலச முடியவில்லை!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "தனிப்பயன் ஊட்டக் கோப்பை அலச முடியவில்லை!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "இயல்புநிலை ஊட்டக் கோப்பை அலச முடியவில்லை!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "விதிகள் கோப்பை அலச முடியவில்லை!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "மாற்றங்களைச் சேமிக்க முடியவில்லை: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "பதிவேற்றும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "தனிப்பயன் ஊட்டக் கோப்பின் பதிவேற்றம் தோல்வியடைந்தது." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "வாய்மொழி பிழைத்திருத்த பதிவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"இந்த எடிட்டர் மூலம் உங்கள் உள்ளக தனிப்பயன் ஊட்டக் கோப்பை பதிவேற்றலாம் அல்லது ஆரம்ப ஒன்றை " +"நிரப்பலாம் (தொகுப்புடன் அனுப்பப்பட்ட பதிப்பின் 1: 1 நகல்). கோப்பு '/etc/banip/" +"banip.custom.feeds' இல் அமைந்துள்ளது. நீங்கள் இந்த கோப்பைத் திருத்தலாம், உள்ளீடுகளை " +"நீக்கலாம், புதியவற்றைச் சேர்க்கலாம் அல்லது உள்ளக காப்புப்பிரதியை செய்யலாம். பராமரிப்பாளர்கள் " +"பதிப்பிற்குச் செல்ல தனிப்பயன் ஊட்டக் கோப்பை அழிக்கவும்." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "விழிப்புணர்வு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "நட்சத்திர தவறான கணக்கு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "தானாக சேர்க்கப்பட்ட ஐபிக்கள் அனுமதிக்க" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "தடுப்பு பட்டியலுக்கு தானாக சேர்க்கப்பட்ட ஐபிக்கள்" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "பானிப்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "ஐ.சி.எம்.பி-வெள்ள பாக்கெட்டுகள் தடுக்கப்பட்டன" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "தவறான சி.டி பாக்கெட்டுகளைத் தடுத்தது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "தவறான TCP பாக்கெட்டுகளைத் தடுத்தது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "தடுக்கப்பட்ட ஒத்திசைவு பாக்கெட்டுகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "தடுக்கப்பட்ட யுடிபி-வெள்ள பாக்கெட்டுகள்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "கிரிட்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "பிழைத்திருத்தம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "துளி" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "கீழ்தோன்றும் தோல்வியுற்றது உள்நுழைவு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "வெளிப்படுகிறது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "பிழை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "தகவல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "உள்ளக இசைவு பட்டியல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "உள்ளக தடுப்பு பட்டியல்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "நினைவகம்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "அறிவிப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "OpenVPN TLS பிழை" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "செயல்திறன்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "நிராகரிக்கவும்" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "SSHD மூடிய இணைப்பு" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "SSHD உள்நுழைவு தோல்வியுற்றது" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "எச்சரிக்கை" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "கணினி பதிவில் சந்தேகத்திற்கிடமான ஐபிக்களைக் கண்டறிய வழக்கமான வெளிப்பாடுகள்." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "தேர்ந்தெடுக்கப்பட்ட பதிவிறக்க பயன்பாட்டிற்கான முன்பே கட்டமைக்கப்பட்ட பதிவிறக்க " +#~ "விருப்பங்களை மேலெழுதவும்." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "ஒவ்வொரு செட் உறுப்புக்கும் NFT கவுண்டரை இயக்கவும்." + +#~ msgid "Set Element Counter" +#~ msgstr "உறுப்பு கவுண்டரை அமைக்கவும்" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "BANIP அட்டவணைக்குள் NFT சங்கிலி முன்னுரிமையை அமைக்கவும், குறைந்த மதிப்புகள் அதிக " +#~ "முன்னுரிமை என்று பொருள்." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "BANIP தொடர்பான தொகுப்புகளுக்கான NFT கொள்கையை அமைக்கவும்." + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx சந்தேகத்திற்கிடமான ஐபி" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "நெறிமுறை/URL வடிவம் ஆதரிக்கப்படவில்லை" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "மின்னஞ்சல் அறிவிப்புகளை இயக்க, 'MSMTP' தொகுப்பை அமைத்து, ஒரு VAILD மின்னஞ்சல் ரிசீவர் " +#~ "முகவரியைக் குறிப்பிடவும்." + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "உள்வரும் மற்றும் வெளிச்செல்லும் ஐபிக்களை பெயரிடப்பட்ட Nftables செட் வழியாகத் தடைசெய்ய " +#~ "BANIP தொகுப்பின் உள்ளமைவு. மேலும் தகவலுக்கு " +#~ "நிகழ்நிலை ஆவணங்கள் " + +#~ msgid "" +#~ msgstr "<தேதி> <இடம்>" + +#~ msgid ", csv" +#~ msgstr ", காபிம" + +#~ msgid ", substring" +#~ msgstr ", அடி மூலக்கூறு" + +#~ msgid "" +#~ msgstr " " + +#~ msgid ", nodups" +#~ msgstr " , முடிச்சுகள்" + +#~ msgid "" +#~ msgstr " <இடம்>" + +#~ msgid ", concatinated" +#~ msgstr " <ச்பேச்>, ஒருங்கிணைந்த" + +#~ msgid "NET" +#~ msgstr " <விண்வெளி> நிகர" + +#~ msgid "YOUR" +#~ msgstr " <இடம்> உங்கள்" + +#~ msgid "[|]" +#~ msgstr " [ | ]" + +#~ msgid ", csv" +#~ msgstr ", காபிம" + +#~ msgid "" +#~ msgstr " " + +#~ msgid "" +#~ msgstr " <இடம்>" + +#~ msgid "Rulev4" +#~ msgstr "Rolev4" + +#~ msgid "Rulev6" +#~ msgstr "Ralev6" + +#~ msgid "Version" +#~ msgstr "பதிப்பு" + +#~ msgid "Elements" +#~ msgstr "கூறுகள்" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "உலகம் வரைபடம் இயக்கப்படவில்லை!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "அறிக்கை கோப்பை அலச முடியவில்லை!" diff --git a/luci-app-secbox/po/templates/banip.pot b/luci-app-secbox/po/templates/banip.pot new file mode 100644 index 000000000..6516bf7db --- /dev/null +++ b/luci-app-secbox/po/templates/banip.pot @@ -0,0 +1,1315 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/tr/secbox.po b/luci-app-secbox/po/tr/secbox.po new file mode 100644 index 000000000..df04e7365 --- /dev/null +++ b/luci-app-secbox/po/tr/secbox.po @@ -0,0 +1,1437 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-08-06 20:04+0000\n" +"Last-Translator: egemen \n" +"Language-Team: Turkish \n" +"Language: tr\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 2026.8.1.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Lütfen seçin (isteğe bağlı) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Seçimi Ayarla --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- varsayılan --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - Afrika ve Hint Okyanusu bölgesine hizmet vermektedir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - Asya Pasifik bölgesine hizmet vermektedir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - Kanada ve Amerika Birleşik Devletleri'ne hizmet vermektedir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN Seçimi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASN'ler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Aktif Aygıtlar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Ektin Beslemeler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Aktif Yukarı Bağlantı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "AdGuardHome giriş hatası" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "BanIP işleme başlamadan önce saniye cinsinden ek tetikleme gecikmesi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Gelişmiş Ayarlar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Protokol ve Bağlantı Noktalarına İzin Ver" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "VLAN Yönlendirmelerine İzin Ver" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "Besleme URL'lerini İzin Ver" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Yalnızca İzin Verilenler Listesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"İzin listesi değişiklikleri kaydedildi, değişikliklerin etkili olması için " +"banIP'yi yeniden yükleyin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"WAN-Giriş ve WAN-İleri zincirinde her zaman belirli bağlantı noktalarına " +"veya bağlantı noktası aralıklarına sahip bir protokole (tcp/udp) izin verin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Her zaman belirli VLAN yönlendirmelerine izin verin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Her zaman belirli VLAN iletimlerini engelle." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Yukarı Bağlantıya Otomatik İzin Ver" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Otomatik İzin Verilenler Listesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Alt Ağı Otomatik Engelle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Otomatik Engelleme Listesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Otomatik Algılama" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Şüpheli IP'ye sahip ek bir RDAP isteğine dayalı olarak tüm alt ağları " +"otomatik olarak engelleme listesi kümesine ekle." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Çözümlenen etki alanlarını ve şüpheli IP'leri otomatik olarak yerel banIP " +"engelleme listesine ekle." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Çözümlenmiş etki alanlarını ve yukarı bağlantı IP'lerini yerel banIP izin " +"verilenler listesine otomatik olarak ekle." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Yedekleme Dizini" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Temel Dizin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "BanIP işlenirken temel çalışma dizini." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "VLAN İletimlerini Engelle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Engelleme Listesi Beslemesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Engellenenler Listesinin Sona Ermesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Engelleme listesi değişiklikleri kaydedildi, değişikliklerin etkili olması " +"için banIP'yi yeniden yükleyin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU Çekirdekleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Vazgeç" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Zincir Önceliği" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Bu sekmedeki değişikliklerin etkili olması için banIP hizmetinin yeniden " +"yüklenmesi gerekir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Bu sekmedeki değişikliklerin etkili olması için banIP hizmetinin yeniden " +"başlatılması gerekir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Temizle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Ülkeler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Özel Akış Düzenleyicisi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Tüm aktif Setlerdeki IP adreslerini tekilleştirin ve yerel engelleme " +"listesini düzenleyin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "IP'leri tekilleştirme" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Açıklama" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"İlgili ağ aygıtlarını, arayüzleri, alt ağları, iletişim kuralları ve " +"yardımcı programları kendiliğinden algıla." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Devre dışı bırak" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "İndirme sırasında SSL sunucu sertifikalarını kontrol etme." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Güvensiz İndir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "İndirme Parametreleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Yeniden İndirme Denemeleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "İndirme Aracı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "E-Posta Bildirimi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "E-Posta Profili" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "E-Posta Alıcı Adresi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "E-Posta Gönderen Adresi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "E mail ayarları" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "E-Posta Konusu" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "İzin Verilenler Listesini Düzenle" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Engellenenler Listesini Düzenle" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Öğe Sayısı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Boş alana izin verilmiyor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Uzak Günlük Kaydını Etkinleştir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "BanIP hizmetini etkinleştirin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Uzak günlük olaylarını almak için cgi arayüzünü etkinleştirin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" +"İşleme hataları durumunda ayrıntılı hata ayıklama günlüğünü etkinleştir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Etkin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "IPv4 desteğini etkinleştirir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "IPv6 desteğini etkinleştirir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" +"Otomatik olarak eklenen engellenenler listesi Kümesi üyelerinin sona erme " +"süresi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Harici İzin Listesi Beslemeleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Harici Engelleme Listesi Beslemeleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Akış Adı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Akış Seçimi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Besleme/Ayarlar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Güvenlik Duvarı Günlüğü" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Bayrak" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Bayrak desteklenmiyor" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Genel Ayarlar" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "LuCI uygulaması banIP'ye erişim izni verin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Yüksek öncelik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "En yüksek öncelik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP Eşiği" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"WAN-DoS saldırılarını önlemek için saniye başına paket cinsinden ICMP eşiği. " +"Bu korumayı devre dışı bırakmak için '0' olarak ayarlayın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP Arama" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP Arama..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4 Ağ Arayüzleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 Desteği" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6 Ağ Arayüzleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 Desteği" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Maksimum açık dosya sayısını artır, örn. Setleri yüklerken geçici bölünmüş " +"dosyaların miktarını yönetmek için." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Bilgi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Geçersiz karakter" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Geçersiz giriş değerleri, değişiklikler kaydedilemiyor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - Latin Amerika ve Karayipler bölgesine hizmet vermektedir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Son çalışma zamanı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "En Az Öncelik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Daha Az Öncelik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Yukarı bağlantı otomatik izin verme işlevini sınırla." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "BanIP başlangıcını tetiklemek için mevcut ağ arayüzlerinin listesi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "BanIP ile ilgili belirli bir kümenin öğelerinin listesi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Yerel Besleme Ayarları" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Logread aracılığıyla standart ayrıştırmayı devre dışı bırakmak için günlük " +"dosyasını, örneğin syslog-ng aracılığıyla ayrıştırma konumu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Günlük Sayısı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Günlük Sınırı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Günlük Ön Yönlendirme" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Günlük Ayarları" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Günlük Şartları" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Günlük Dosyası Konumu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Maksimum Açık Dosya Sayısı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT Bilgileri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "NFT Günlük Kaydı Seviyesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Ağ Aygıtları" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Güzel Düzey" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Normal Öncelik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Hata durumunda indirme denemelerinin sayısı (uclient-fetch tarafından " +"desteklenmez)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Engellemeden önce günlükteki aynı IP ile yapılan başarısız oturum açma " +"denemelerinin sayısı." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Genel bakış" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Şüpheli olaylar için yalnızca belirtilen son sayıdaki günlük girişini " +"ayrıştırın. Günlük izleyiciyi tamamen devre dışı bırakmak için onu '0' " +"olarak ayarlayın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "İşlem Günlüğü" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "BanIP bildirim e-postaları için 'msmtp' tarafından kullanılan profil." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - Avrupa, Orta Doğu ve Orta Asya'ya hizmet vermektedir" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Her banIP çalıştırmasında E-Posta bildirimleri alın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"banIP bildirim E-Postaları için alıcı adresi, bu bilgi E-Posta işlevini " +"etkinleştirmek için gereklidir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Yenile" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Bölgesel İnternet Tescili" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Uzak Belirteç" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Rapor Dizini" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "İnternet erişimini az sayıda güvenli IP'ye kısıtlayın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Sonuç" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Bayrakları Çalıştır" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Çalıştırma Bilgileri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN Eşiği" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"WAN-DoS saldırılarını önlemek için saniye başına paket cinsinden SYN eşiği. " +"Bu korumayı devre dışı bırakmak için '0' olarak ayarlayın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Kaydet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Belirli bir IP için banIP ile ilgili Setleri ara." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Önceden yapılandırılmış indirme yardımcı programlarından birini seçin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "WAN ağ aygıt(lar)ını seçin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Mantıksal WAN IPv4 ağ arayüzünü/arayüzlerini seçin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Mantıksal WAN IPv6 ağ arayüzünü/arayüzlerini seçin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "BanIP bildirim e-postaları için gönderen adresi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Ayarla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Politika Belirleyin" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Raporlamayı Ayarla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Bölme Boyutunu Ayarla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Ayrıntıları ayarla" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "NFT günlüğü için sistem günlüğü düzeyini ayarlayın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Ayarlar" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Başlangıç Tetikleme Arayüzü" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Durum" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Dur" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Alt ağ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Tablo/Zincir Ayarları" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "banIP ile ilgili rapor dosyaları için hedef dizin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Sıkıştırılmış akış yedeklemeleri için hedef dizin." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" +"İzin verilenler listesi çok büyük olduğundan değişiklikler kaydedilemiyor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" +"Engellenenler listesi çok büyük olduğundan değişiklikler kaydedilemiyor." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Seçilen öncelik banIP'in arka planda işlenmesi için kullanılacaktır." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Bu, belirli MAC, IP adresleri veya alan adlarına izin verecek yerel banIP " +"izin verilenler listesidir.
Lütfen unutmayın: yalnızca " +"tam olarak bir MAC/IPv4/IPv6 ekleyin satır başına adres veya alan adı. CIDR " +"gösterimindeki ve MAC/IP bağlamalarındaki aralıklara izin verilir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Bu, belirli MAC, IP adresleri veya alan adlarını engelleyecek yerel banIP " +"engelleme listesidir.
Lütfen unutmayın: yalnızca tam " +"olarak bir MAC/IPv4/IPv6 ekleyin satır başına adres veya alan adı. CIDR " +"gösterimindeki ve MAC/IP bağlamalarındaki aralıklara izin verilir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Zaman damgası" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "cgi arayüzü ile iletişim kurmak için belirteç." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "BanIP bildirim e-postaları için konu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Tetikleme Gecikmesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP Eşiği" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"WAN-DoS saldırılarını önlemek için saniye başına paket cinsinden UDP eşiği. " +"Bu korumayı devre dışı bırakmak için '0' olarak ayarlayın." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Ülkeler dosyası ayrıştırılamıyor!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Özel akış dosyası ayrıştırılamıyor!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Varsayılan akış dosyası ayrıştırılamıyor!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Kural kümesi dosyası ayrıştırılamıyor!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Düzenlemeler kaydedilemedi: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Özel akış dosyasının yüklenmesi başarısız oldu." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Ayrıntılı Hata Ayıklama Günlüğü" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"Bu düzenleyici ile yerel özel akış dosyanızı yükleyebilir veya ilk dosyayı " +"doldurabilirsiniz (paketle birlikte gönderilen sürümün bire bir kopyası). " +"Dosya '/etc/banip/banip.custom.feeds' adresinde bulunur. Daha sonra bu " +"dosyayı düzenleyebilir, girdileri silebilir, yenilerini ekleyebilir veya " +"yerel bir yedekleme yapabilirsiniz. Geliştirici sürümüne geri dönmek için " +"özel akış dosyasını temizlemeniz yeterlidir." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "uyarı" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "izin listesine otomatik eklenen IP'ler" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "engelleme listesine otomatik eklenen IP'ler" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "engellenen icmp-flood paketleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "engellenen geçersiz ct paketleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "geçersiz tcp paketlerini engelledi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "engellenen syn-flood paketleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "engellenen udp-flood paketleri" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "kritik" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "hata ayıklama" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "bırak" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "ortaya çıkan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "hata" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "bilgi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "yerel izin verilenler listesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "yerel engellenenler listesi" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "bellek" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "ikaz" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "performans" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "reddet" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "uyar" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Seçilen indirme yardımcı programı için önceden yapılandırılmış indirme " +#~ "seçeneklerini geçersiz kıl." + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "BanIP tablosunda nft zincir önceliğini ayarlayın, düşük değerler daha " +#~ "yüksek öncelik anlamına gelir." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "banIP ile ilgili Setler için nft politikasını ayarlayın." + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Protokol/URL formatı desteklenmiyor" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "E-posta bildirimlerini etkinleştirmek için 'msmtp' paketini kurun ve " +#~ "geçerli bir E-Posta alıcı adresi belirtin." + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Adlandırılmış nftables Setleri aracılığıyla gelen ve giden IP'leri " +#~ "yasaklamak için banIP paketinin yapılandırılması. Daha fazla bilgi için " +#~ "lütfençevrimiçi dokümantasyon" + +#~ msgid "Rulev4" +#~ msgstr "Kuralv4" + +#~ msgid "Rulev6" +#~ msgstr "Kuralv6" + +#~ msgid "Version" +#~ msgstr "Versiyon" + +#~ msgid "Elements" +#~ msgstr "Öğeler" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Rapor dosyası ayrıştırılamıyor!" diff --git a/luci-app-secbox/po/uk/secbox.po b/luci-app-secbox/po/uk/secbox.po new file mode 100644 index 000000000..3c4616c6e --- /dev/null +++ b/luci-app-secbox/po/uk/secbox.po @@ -0,0 +1,1550 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-07-25 16:02+0000\n" +"Last-Translator: Dan \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\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" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Виберіть (необовʼязково) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "— Вибір набору —" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- типово --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "<Рядок JSON><Рядок JSON>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "<Синтаксис сурікати>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC — обслуговує Африку та регіон Індійського океану" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC — обслуговує Азійсько-Тихоокеанський регіон" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN — обслуговує Канаду та США" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "Вибір ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "Номери ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Активні пристрої" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Активні канали" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Активний висхідний канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "Помилка входу в AdGuardHome" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" +"Додаткова затримка спрацьовування в секундах перед початком обробки banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Розширені налаштування" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "Дозволити протокол/порти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "Дозволити переспрямування VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "URL-адреси каналів білого списку" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Лише білий список" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Зміни білого списку збережено, перезавантажте banIP, щоб зміни вступили в " +"силу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"Завжди дозволяти протокол (tcp/udp) із певними портами або діапазонами " +"портів у ланцюзі WAN-Input і WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "Завжди дозволяти певні переспрямування VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "Завжди блокувати певні переспрямування VLAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Автоматичний дозвіл висхідного каналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Автоматичний білий список" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Автоматичне блокування підмережі" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Автоматичний чорний список" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Автоматичне визначення" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Автоматично додавати цілі підмережі до списку заблокованих на основі " +"додаткового запиту RDAP із підозрілою IP-адресою." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Автоматично додавати визначені домени та підозрілі IP-адреси до локального " +"чорного списку banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Автоматично додавати визначені домени та IP-адреси висхідних каналів до " +"локального білого списку banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Каталог резервних копій" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Базовий каталог" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Базовий робочий каталог під час обробки banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "Блокувати переспрямування VLAN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" +"Блокувати пакети з підробленими вихідними IP-адресами у всіх підтримуваних " +"ланцюжках." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Канал чорного списку" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Термін дії елементів у наборі блокувань" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" +"Зміни чорного списку збережено, перезавантажте banIP, щоб зміни набули " +"чинності." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "Розмір сплеску в пакетах для спільного NFT-обмеження журналу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Ядра ЦП" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "Скасувати" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "Ланцюг" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "Пріоритет ланцюга" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" +"Щоб зміни на цій вкладці набули чинності, потрібно перезавантажити службу " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Щоб зміни на цій вкладці набули чинності, потрібно перезапустити службу " +"banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Очистити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "Збір IP-адрес на основі номерів автономних систем." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" +"Налаштування пакета banIP для блокування вхідних і вихідних IP-адрес через " +"іменовані nftables Sets. Додаткові відомості — %s." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Кількість" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Країни" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "Вибір країни" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Користувацький редактор каналів" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Дедуплікувати IP-адреси в усіх активних наборах і навести порядок у " +"локальному списку блокувань." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Дедуплікувати IP-адреси" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Опис" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Визначати відповідні мережеві пристрої, інтерфейси, підмережі, протоколи та " +"утиліти автоматично." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Вимкнути" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Не перевіряти SSL-сертифікати сервера під час завантаження." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Завантажити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Небезпечне завантаження" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Параметри завантаження" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Повторні спроби завантаження" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Утиліта для завантаження" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "Відкидати пакети мовчки або активно відхиляти вхідний трафік." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Сповіщення електронною поштою" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Профіль електронної пошти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Адреса отримувача електронної пошти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Адреса відправника електронної пошти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Налаштування електронної пошти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Тема листа" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Редагувати білий список" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Редагувати чорний список" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Кількість елементів" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "Елементи (макс. 50)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Порожнє поле не допускається" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "Увімкнути BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "Увімкнути карту GeoIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" +"Увімкнути лічильники NFT для елементів Set і правил ланцюжків. Потрібно для " +"GeoIP-карти та статистики пакетів у звітах Set." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "Увімкнути віддалене журналювання" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" +"Увімкнути карту GeoIP із підозрілими елементами набору. Для цього потрібні " +"зовнішні запити для отримання фрагментів карти та даних геолокації." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Увімкнути службу banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "Увімкнути інтерфейс CGI для отримання подій віддаленого журналювання." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Увімкнути докладне журналювання налагодження у разі помилок обробки." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Увімкнено" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Вмикає підтримку IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Вмикає підтримку IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "Каталог помилок" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "Термін дії для автоматично доданих учасників списку блокування." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "Канали зовнішнього білого списку" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Канали зовнішніх чорних списків" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "Не вдалося згенерувати звіт banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "Канал повний" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "Скидання прапора каналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Назва каналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Вибір каналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "Налаштування каналу/набору" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Заповнити" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Журнал брандмауера" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Прапорець" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Прапорець не підтримується" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Загальні налаштування" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Надати доступ до програми LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Високий пріоритет" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Найвищий пріоритет" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP-поріг" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"ICMP-Threshold у пакетах за секунду для запобігання атак WAN-DoS. Щоб " +"вимкнути цей захист, встановіть для нього значення «0»." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Пошук IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Пошук IP…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "Мережеві інтерфейси IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Підтримка IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "Мережеві інтерфейси IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Підтримка IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "Вхідний" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "Вхідні та вихідні" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "Вхідний і вихідний канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "Політика блокування вхідних повідомлень" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "Вхідний канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "Вхідні (пакети)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Збільшити максимальну кількість відкритих файлів, наприклад для обробки " +"кількості тимчасових розділених файлів під час завантаження наборів." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Інформація" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "Неправильний формат URL" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Недійсні символи" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "Неправильний формат терміну дії, наприклад 5m, 2h, 1d або 1h30m" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Недійсні введені значення, неможливо зберегти зміни." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC — обслуговує країни Латинської Америки та Карибського басейну" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Останній запуск" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Найменший пріоритет" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Менший пріоритет" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" +"Обмежує кількість ядер процесора, що використовуються banIP, для економії " +"оперативної памʼяті; автоматично визначається типово." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Обмежити функцію автоматичного дозволу висхідного каналу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "Список доступних мережевих інтерфейсів для запуску banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Перелічити елементи певного набору, повʼязаного з banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "Налаштування локального каналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" +"Розташування для аналізу файлу журналу, наприклад через syslog-ng, щоб " +"вимкнути стандартний аналіз через logread." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "Обмеження сплеску журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Кількість записів журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "Журналювати вхідні" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Ліміт журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "Журналювати вихідні" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "Журналювати Prerouting" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "Обмеження швидкості журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Налаштування журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Терміни журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "Журналювати підозрілі пакети у ланцюжку LAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "Журналювати підозрілі пакети у ланцюжку Prerouting." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "Журналювати підозрілі пакети у ланцюжку WAN-Input і WAN-Forward." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "Розташування файлу журналу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "Невдалий вхід у LuCI" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Скидання карти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "Карта…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Максимально відкриті файли" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Інформація про NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "Рівень журналу NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Мережеві пристрої" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "Помилка мережі" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Рівень nice" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Поки що немає журналів, повʼязаних з %s!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Немає даних карти GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "Конфігурацію banIP не знайдено!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Звичайний пріоритет" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "Кількість спроб завантаження Set у разі помилки." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Кількість спроб завантаження у разі помилки (не підтримується uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Кількість невдалих спроб входу з тієї самої IP-адреси в журналі до " +"блокування." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "Виключити певні канали з процесу дедуплікації." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "Вихідний" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "Вихідний канал" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "Вихідні (пакети)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" +"Замінити типову конфігурацію каналу й застосувати канал до вхідного та " +"вихідного ланцюжка." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" +"Замінити типову конфігурацію каналу й застосовувати канал лише до вхідного " +"ланцюжка." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" +"Замінити типову конфігурацію каналу й застосовувати канал лише до вихідного " +"ланцюжка." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" +"Перевизначити типову конфігурацію каналу й видалити наявні обмеження порту/" +"протоколу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Перевизначте попередньо налаштовані параметри завантаження для вибраної " +"утиліти. Прапорець виводу, наприклад, «-o» для curl або «-O» для wget, має " +"бути останнім параметром." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Огляд" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Аналізувати лише останню зазначену кількість записів журналу на наявність " +"підозрілих подій. Щоб взагалі вимкнути монітор журналу, встановіть для нього " +"значення «0»." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "Порт / Протокол" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Журнал обробки" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" +"Профіль, який використовується «msmtp» для електронних повідомлень про " +"заборону IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE — обслуговує Європу, Близький Схід і Центральну Азію" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" +"Швидкість (за секунду) для спільного NFT-обмеження журналу, що " +"застосовується глобально до всіх журнальних правил. Встановіть «0», щоб " +"повністю вимкнути обмеження швидкості — наприклад, якщо використовуєте ulogd " +"чи інші обробники журналів простору користувача." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Отримувати сповіщення електронною поштою щоразу під час запуску banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Адреса одержувача електронної пошти зі сповіщеннями про заборону IP. Ця " +"інформація потрібна для ввімкнення функції електронної пошти." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Оновити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "Регіональний Інтернет-реєстр" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" +"Регулярні вирази для виявлення підозрілих IP-адрес у системному журналі. " +"Використовується остання IP-адреса в рядку; для форматів, де джерело вказано " +"першим (наприклад, nginx), додайте префікс «first:»." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "Віддалений маркер" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "Подія віддаленого журналювання" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Каталог звітів" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "Лічильники звітів" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" +"Обмежити доступ до Інтернету з/до невеликої кількості безпечних IP-адрес." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Результат" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Правило" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Прапорці запуску" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Інформація про запуск" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN-поріг" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"SYN-Threshold у пакетах за секунду для запобігання атак WAN-DoS. Щоб " +"вимкнути цей захист, встановіть для нього значення «0»." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Зберегти" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Зберегти та перезавантажити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Зберегти та перезапустити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "Пошук IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "Триває пошук, зачекайте…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Шукати певну IP-адресу в наборах, повʼязаних із banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "Час очікування пошуку вичерпано." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Вибрати одну з попередньо налаштованих утиліт завантаження." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Вибрати пристрій(-ї) мережі WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Вибрати логічні мережеві інтерфейси WAN IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Вибрати логічні мережеві інтерфейси WAN IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Адреса відправника для електронних повідомлень про заборону IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Набір" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "Вміст набору" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "Вміст набору…" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "Повторні спроби завантаження набору" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "Політика набору" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Звітність по наборах" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Розмір розділення набору" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Деталі набору" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" +"Задати пріоритет NFT-ланцюжка в таблиці banIP — менші значення означають " +"вищий пріоритет." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "Задати NFT-політику для наборів, повʼязаних з banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Задати рівень системного журналу для журналювання NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Налаштування" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "Показати вміст" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "Показувати лише елементи набору з відповідями" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "Розділений набір ASN" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "Розділений набір країн" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" +"Розділяти завантаження зовнішнього набору після кожних n елементів для " +"економії оперативної памʼяті; типово вимкнено." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Інтерфейс тригера запуску" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Стан" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Зупинити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Підмережа" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" +"Зведена інформація про країни на основі даних Регіонального інтернет-реєстру " +"(RIR)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Інформація про систему" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "Налаштування таблиці/ланцюга" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "Цільовий каталог для файлів помилок, повʼязаних із banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Цільовий каталог для файлів звітів, повʼязаних із banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Цільовий каталог для резервних копій стисненого каналу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Список дозволених завеликий, неможливо зберегти зміни." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Список блокування завеликий, неможливо зберегти зміни." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "Вибрані номери ASN зберігаються в окремих наборах." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "Вибрані країни зберігаються в окремих наборах." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Вибраний пріоритет буде використано для фонової обробки banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" +"Вивід системного журналу, попередньо відфільтрований на наявність " +"повідомлень, повʼязаних з: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Це локальний список дозволених IP-адрес, який дозволятиме певні MAC-, IP-" +"адреси або доменні імена.
Зверніть увагу: додайте лише " +"одну MAC/IPv4/IPv6-адресу або доменне імʼя в рядок. Дозволено діапазони в " +"нотації CIDR і привʼязки MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Це локальний список заборонених IP-адрес, який забороняє певні MAC-, IP-" +"адреси чи доменні імена.
Зверніть увагу: додайте лише " +"одну MAC/IPv4/IPv6-адресу або доменне імʼя в рядок. Дозволено діапазони в " +"нотації CIDR і привʼязки MAC/IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" +"Цей звіт показує останню статистику наборів NFT; натисніть кнопку «Оновити», " +"щоб отримати нову. Також можна відобразити конкретний вміст наборів, шукати " +"підозрілі IP-адреси і, нарешті, відобразити їх на карті." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Мітка часу" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" +"Щоб увімкнути сповіщення електронною поштою, налаштуйте пакет «msmtp» і " +"вкажіть дійсну адресу одержувача." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "Маркер для звʼязку з інтерфейсом cgi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Тема для електронних повідомлень про заборону IP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Затримка запуску" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP-поріг" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"UDP-Threshold у пакетах за секунду для запобігання атак WAN-DoS. Щоб " +"вимкнути цей захист, встановіть для нього значення «0»." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "Не вдалося проаналізувати інформацію виконання banIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "Неможливо проаналізувати файл країн!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Неможливо проаналізувати файл користувацького каналу!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Неможливо проаналізувати файл типового каналу!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "Не вдалося проаналізувати файл набору правил!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Не вдалося зберегти зміни: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Вивантажити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Не вдалося вивантажити файл користувацького каналу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Докладне журналювання налагодження" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" +"За допомогою цього редактора можна передати свій локальний користувацький " +"файл каналу або заповнити початковий файл (копія 1:1 версії, що постачається " +"з пакетом). Файл розташовано за адресою «/etc/banip/banip.custom.feeds». " +"Потім можна редагувати цей файл, видаляти записи, додавати нові або " +"створювати локальну резервну копію. Щоб повернутися до версії від " +"супроводжувачів, просто очистіть файл користувацького каналу." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "тривога" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "зірочка недійсний обліковий запис" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "автоматично додані IP-адреси до білого списку" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "автоматично додані IP-адреси до чорного списку" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "заблоковані пакети bcp38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "заблоковані пакети icmp-flood" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "заблоковано недійсні пакети ct" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "заблоковано недійсні пакети tcp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "заблоковані пакети syn-flood" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "заблоковані пакети udp-flood" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "критичний" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "налагодження" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "відкинути" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "невдалий вхід через dropbear" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "аварія" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "помилка" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "інформація" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "локальний білий список" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "локальний список блокувань" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "памʼять" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "сповіщення" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "онлайн-документація" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "помилка TLS у openvpn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "продуктивність" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "відхилити" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd: закрите зʼєднання" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "невдалий вхід через sshd" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "попередження" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "" +#~ "Регулярні вирази для виявлення підозрілих IP-адрес у системному журналі." + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Перевизначити попередньо налаштовані параметри завантаження для вибраної " +#~ "утиліти завантаження." + +#~ msgid "Enable nft counter for every Set element." +#~ msgstr "Увімкнути лічильник nft для кожного елемента Set." + +#~ msgid "Set Element Counter" +#~ msgstr "Встановити лічильник елементів" + +#~ msgid "" +#~ "Set the nft chain priority within the banIP table, lower values means " +#~ "higher priority." +#~ msgstr "" +#~ "Встановіть пріоритет ланцюжка nft у таблиці banIP, менші значення " +#~ "означають вищий пріоритет." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Налаштуйте політику nft для наборів, пов’язаних із banIP." + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx підозрілий IP" + +#~ msgid " " +#~ msgstr " <Мережева маска>" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Формат протоколу/URL не підтримується" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Щоб увімкнути сповіщення електронною поштою, налаштуйте пакет «msmtp» і " +#~ "вкажіть дійсну адресу одержувача електронної пошти." + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "Не вдалося проаналізувати інформацію про виконання!" + +#~ msgid "" +#~ "Configuration of the banIP package to ban incoming and outgoing IPs via " +#~ "named nftables Sets. For further information please check the online documentation" +#~ msgstr "" +#~ "Конфігурація пакета banIP для заборони вхідних і вихідних IP-адрес через " +#~ "іменовані набори nftables. Щоб отримати додаткову інформацію, перегляньте " +#~ "онлайн-документація" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", підрядок" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", вузлики" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", зчеплені" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "YOUR" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Rulev4" +#~ msgstr "Rulev4" + +#~ msgid "Rulev6" +#~ msgstr "Rulev6" + +#~ msgid "Version" +#~ msgstr "Версія" + +#~ msgid "logread not found on system." +#~ msgstr "Зчитування журналу не знайдено в системі." + +#~ msgid "banIP Log" +#~ msgstr "Журнал banIP" + +#~ msgid "banIP firewall Log" +#~ msgstr "Журнал брандмауера banIP" + +#~ msgid "Elements" +#~ msgstr "Елементи" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "Карта GeoIP не ввімкнена!" + +#~ msgid "Unable to parse the report file!" +#~ msgstr "Неможливо проаналізувати файл звіту!" diff --git a/luci-app-secbox/po/vi/secbox.po b/luci-app-secbox/po/vi/secbox.po new file mode 100644 index 000000000..37dda62de --- /dev/null +++ b/luci-app-secbox/po/vi/secbox.po @@ -0,0 +1,1384 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-06-09 01:01+0000\n" +"Last-Translator: Mytai20100 \n" +"Language-Team: Vietnamese \n" +"Language: vi\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 2026.6\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- Chọn một lựa chọn (Không bắt buộc) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- Cài đặt lựa chọn --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "--Mặc định--" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "Thiết bị hoạt động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "Nguồn cấp đang hoạt động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "Uplink hoạt động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "Cài đặt Nâng cao" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "Chỉ sử dụng Allowlist" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "Tự động cho phép Uplink" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "Allowlist tự động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "Chặn Subnet tự động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "Blocklist tự động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "Phát hiện tự động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" +"Tự động thêm toàn bộ các subnet vào Blocklist dựa trên yêu cầu RDAP bổ sung " +"với IP đáng ngờ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" +"Tự động thêm tên miền đã được giải quyết và các IP đáng ngờ vào danh sách " +"chặn banIP cục bộ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" +"Tự động thêm tên miền đã được giải quyết và các IP uplink vào danh sách cho " +"phép banIP cục bộ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "Thư mục sao lưu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "Thư mục gốc" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "Thư mục làm việc cơ sở trong quá trình xử lý banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "Nguồn cấp danh sách chặn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "Hết hạn của Blocklist Set" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "Số lõi CPU" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "huỷ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" +"Các thay đổi trên tab này cần khởi động lại dịch vụ banIP để có hiệu lực." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "Xóa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "Đếm" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "Quốc gia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "Trình chỉnh sửa nguồn cấp tùy chỉnh" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" +"Loại bỏ trùng lặp địa chỉ IP trên tất cả các Sets hoạt động và dọn dẹp danh " +"sách chặn cục bộ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "Loại bỏ trùng lặp IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "Mô tả" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" +"Tự động phát hiện các thiết bị, giao diện, mạng con, giao thức và tiện ích " +"mạng liên quan." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "Vô hiệu hóa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "Không kiểm tra chứng chỉ máy chủ SSL trong khi tải xuống." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "Tải xuống" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "Tải về không bảo mật" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "Tham số tải xuống" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "Số lần thử tải xuống" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "Tiện ích tải xuống" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "Thông báo Email" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "Hồ sơ Email" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "Địa chỉ người nhận Email" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "Địa chỉ người gửi Email" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "Cài đặt E-Mail" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "Chủ đề Email" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "Chỉnh sửa danh sách cho phép" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "Chỉnh sửa danh sách chặn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "Số lượng phần tử" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "Không được để trống chỗ này" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "Bật dịch vụ banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "Bật ghi nhật ký debug chi tiết trong trường hợp xảy ra lỗi xử lý." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "Đã bật" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "Bật hỗ trợ IPv4." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "Bật hỗ trợ IPv6." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "Thời gian hết hạn cho các thành viên danh sách chặn tự động thêm." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "Nguồn cấp danh sách chặn bên ngoài" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "Tên nguồn cấp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "Chọn nguồn cấp" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "Điền" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "Nhật ký tường lửa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "Cờ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "Không hỗ trợ cờ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "Các cài đặt chung" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "Cấp quyền truy cập cho ứng dụng LuCI banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "Ưu tiên cao" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "Ưu tiên cao nhất" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "Tìm kiếm IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "Tìm kiếm IP..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "Hỗ trợ IPv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "Hỗ trợ IPv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" +"Tăng số tệp mở tối đa, ví dụ: để xử lý số lượng tệp chia tạm thời khi tải " +"các Sets." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "Thông Tin" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "Ký tự không hợp lệ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "Giá trị đầu vào không hợp lệ, không thể lưu thay đổi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "Lần chạy cuối cùng" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "Ưu tiên thấp nhất" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "Ưu tiên thấp hơn" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "Giới hạn chức năng uplink autoallow." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "Danh sách các giao diện mạng có sẵn để kích hoạt khởi động banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "Liệt kê các phần tử của một Set liên quan đến banIP cụ thể." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "Số lượng Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "Giới hạn ghi log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "Cài đặt Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "Điều khoản Log" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "Đặt lại ánh xạ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "Số tệp Mở Tối đa" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "Thông tin NFT" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "Thiết bị Mạng" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Mức độ ưu tiên (Nice)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "Chưa có nhật ký liên quan đến %s nào!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "Không có dữ liệu bản đồ GeoIP!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "Ưu tiên bình thường" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" +"Số lần thử tải xuống khi gặp lỗi (không được hỗ trợ bởi uclient-fetch)." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" +"Số lần đăng nhập không thành công của cùng một IP trong log trước khi chặn." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" +"Ghi đè các tùy chọn tải xuống đã được cấu hình sẵn cho tiện ích tải xuống đã " +"chọn. Cờ đầu ra, ví dụ: '-o' cho curl hoặc '-O' cho wget, phải là tham số " +"cuối cùng." + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "Tổng quan" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" +"Chỉ phân tích một số lượng cuối cùng của các mục log để xác định sự kiện " +"đáng ngờ. Để vô hiệu hóa giám sát log hoàn toàn, đặt giá trị là '0'." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "Log Xử lý" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "Hồ sơ được sử dụng bởi 'msmtp' cho thư thông báo banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "Nhận thông báo qua E-Mail mỗi khi chạy banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" +"Địa chỉ người nhận cho thư thông báo banIP, thông tin này là bắt buộc để " +"kích hoạt chức năng E-Mail." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "Làm mới" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "Đường dẫn Report" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "Hạn chế truy cập internet từ/đến một số IP an toàn nhỏ." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "Kết quả" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "Quy tắc" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "Run flags" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "Thông tin chạy" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "Lưu" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "Lưu & Tải lại" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "Lưu & Khởi động lại" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "Tìm kiếm các Bộ liên quan đến banIP cho một IP cụ thể." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "Chọn một trong các tiện ích tải xuống được cấu hình trước." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "Chọn thiết bị mạng WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "Chọn giao diện mạng IPv4 logic của WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "Chọn giao diện mạng IPv6 logic của WAN." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "Địa chỉ người gửi cho thư thông báo banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "Đặt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "Đặt báo cáo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "Đặt kích thước phân chia" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "Chi tiết đặt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "Đặt mức syslog cho việc ghi log NFT." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "Cài đặt" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "Giao diện kích hoạt khởi động" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "Trạng thái" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "Dừng" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "Mạng con" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "Thông tin hệ thống" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "Thư mục đích cho các tệp báo cáo liên quan đến banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "Thư mục đích cho sao lưu nguồn cấp dữ liệu được nén." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "Danh sách cho phép quá lớn, không thể lưu các sửa đổi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "Danh sách chặn quá lớn, không thể lưu các sửa đổi." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "Ưu tiên được chọn sẽ được sử dụng cho xử lý nền banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "Đầu ra syslog, được lọc trước cho các thông báo liên quan đến: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Đây là danh sách cho phép banIP cục bộ sẽ cho phép các địa chỉ MAC/IP/CIDR " +"cụ thể.
Vui lòng lưu ý: chỉ thêm chính xác một địa chỉ " +"MAC/IPv4/IPv6 hoặc tên miền trên mỗi dòng." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" +"Đây là danh sách chặn banIP cục bộ sẽ ngăn chặn các địa chỉ MAC/IP/CIDR cụ " +"thể.
Vui lòng lưu ý: chỉ thêm chính xác một địa chỉ " +"MAC/IPv4/IPv6 hoặc tên miền trên mỗi dòng." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "Dấu thời gian" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "Chủ đề cho Email thông báo banIP." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "Kích hoạt độ trễ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "Không thể phân tích tệp nguồn cấp tùy chỉnh!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "Không thể phân tích tệp nguồn cấp mặc định!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "Không thể lưu các sửa đổi: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "Tải lên" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "Tải lên tệp nguồn cấp tùy chỉnh thất bại." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "Ghi nhật ký gỡ lỗi chi tiết (Verbose Debug Logging)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "cảnh báo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "crit" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "debug" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "emerg" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "err" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "info" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "danh sách cho phép cục bộ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "danh sách chặn cục bộ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "bộ nhớ" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "thông báo" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "tài liệu trực tuyến" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "hiệu suất" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "cảnh báo" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "" +#~ "Ghi đè các tùy chọn tải xuống được cấu hình trước cho tiện ích tải xuống " +#~ "được chọn." + +#~ msgid "Set the nft policy for banIP-related Sets." +#~ msgstr "Đặt chính sách nft cho các Bộ liên quan đến banIP." + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "Định dạng giao thức/URL không được hỗ trợ" + +#~ msgid "" +#~ "To enable email notifications, set up the 'msmtp' package and specify a " +#~ "vaild E-Mail receiver address." +#~ msgstr "" +#~ "Để bật thông báo qua email, thiết lập gói 'msmtp' và chỉ định địa chỉ " +#~ "email người nhận hợp lệ." + +#~ msgid "Rulev4" +#~ msgstr "Quy tắc v4" + +#~ msgid "Rulev6" +#~ msgstr "Quy tắc v6" + +#~ msgid "Version" +#~ msgstr "Phiên bản" + +#~ msgid "Elements" +#~ msgstr "Các phần tử" diff --git a/luci-app-secbox/po/yua/secbox.po b/luci-app-secbox/po/yua/secbox.po new file mode 100644 index 000000000..93a854818 --- /dev/null +++ b/luci-app-secbox/po/yua/secbox.po @@ -0,0 +1,1318 @@ +msgid "" +msgstr "" +"Language: yua\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "" diff --git a/luci-app-secbox/po/zh-cn/secbox.po b/luci-app-secbox/po/zh-cn/secbox.po new file mode 100644 index 000000000..be5e0343b --- /dev/null +++ b/luci-app-secbox/po/zh-cn/secbox.po @@ -0,0 +1,1524 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_Hans\n" + +msgid "-- Please choose (optional) --" +msgstr "-- 请选择(可选) --" + +msgid "-- Set Selection --" +msgstr "-- IP 集选择 --" + +msgid "-- default --" +msgstr "-- 默认 --" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - 服务非洲和印度洋区域" + +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - 服务亚太区域" + +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - 服务加拿大和美国" + +msgid "ASN Selection" +msgstr "ASN 选择" + +msgid "ASNs" +msgstr "平均取样数" + +msgid "Active Devices" +msgstr "活动设备" + +msgid "Active Feeds" +msgstr "活动源" + +msgid "Active Uplink" +msgstr "活跃的上行线路" + +msgid "AdGuardHome login error" +msgstr "AdGuardHome 登录错误" + +msgid "Additional trigger delay in seconds before SecBox processing begins." +msgstr "SecBox 处理开始之前的额外触发延迟(以秒为单位)。" + +msgid "Advanced Settings" +msgstr "高级设置" + +msgid "Allow Protocol/Ports" +msgstr "允许协议/端口" + +msgid "Allow VLAN Forwards" +msgstr "允许 VLAN 转发" + +msgid "Allowlist Feed URLs" +msgstr "白名单源 URL" + +msgid "Allowlist Only" +msgstr "仅白名单" + +msgid "Allowlist modifications have been saved, reload SecBox that changes take effect." +msgstr "允许列表的修改已保存,重新加载 SecBox 以使更改生效。" + +msgid "Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-Input and WAN-Forward chain." +msgstr "在 WAN-Input 和 Wan-Forward 链中始终允许协议(tcp/udp)特定端口或端口区间。" + +msgid "Always allow certain VLAN forwards." +msgstr "始终允许特定的 VLAN 转发。" + +msgid "Always block certain VLAN forwards." +msgstr "始终拦截特定的 VLAN 转发。" + +msgid "Auto Allow Uplink" +msgstr "自动允许上行线路" + +msgid "Auto Allowlist" +msgstr "自动白名单" + +msgid "Auto Block Subnet" +msgstr "自动拦截子网" + +msgid "Auto Blocklist" +msgstr "自动黑名单" + +msgid "Auto Detection" +msgstr "自动检测" + +msgid "Automatically add entire subnets to the blocklist Set based on an additional RDAP request with the suspicious IP." +msgstr "基于和可疑 IP 地址的额外 RDAP 请求自动添加条目子网到黑名单集。" + +msgid "Automatically add resolved domains and suspicious IPs to the local SecBox blocklist." +msgstr "自动添加已解析域名和可疑 IP 到本地 SecBox 黑名单。" + +msgid "Automatically add resolved domains and uplink IPs to the local SecBox allowlist." +msgstr "自动添加已解析域名和上行线路 IP 到本地 SecBox 白名单。" + +msgid "Backup Directory" +msgstr "备份目录" + +msgid "Base Directory" +msgstr "工作目录" + +msgid "Base working directory while SecBox processing." +msgstr "SecBox 处理时的基础工作目录。" + +msgid "Block VLAN Forwards" +msgstr "拦截 VLAN 转发" + +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "在所有支持的链中拦截伪造源 IP 地址的数据包。" + +msgid "Blocklist Feed" +msgstr "黑名单源" + +msgid "Blocklist Set Expiry" +msgstr "黑名单集过期时间" + +msgid "Blocklist modifications have been saved, reload SecBox that changes take effect." +msgstr "阻止列表的修改已保存, 重新加载 SecBox 以使更改生效。" + +msgid "Burst size in packets for the shared NFT log limit." +msgstr "共享 NFT 日志限制的 Burst 数据包大小。" + +msgid "CPU Cores" +msgstr "CPU 核心数" + +msgid "Cancel" +msgstr "取消" + +msgid "Chain" +msgstr "链" + +msgid "Chain Priority" +msgstr "链优先级" + +msgid "Changes on this tab needs a SecBox service reload to take effect." +msgstr "此标签页上的更改需要重新加载 SecBox 服务方能生效。" + +msgid "Changes on this tab needs a SecBox service restart to take effect." +msgstr "此标签页上进行的更改需要重启 SecBox 服务才能生效。" + +msgid "Clear" +msgstr "清除" + +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "基于 ASN 号码的 IP 地址集。" + +msgid "Configuration of the SecBox package to ban incoming and outgoing IPs via named nftables Sets. For further information please check the %s." +msgstr "配置 SecBox 包通过命名的 nftables 集封禁入站和出站 IP。进一步信息请见 %s。" + +msgid "Count" +msgstr "计数" + +msgid "Countries" +msgstr "国家或地区" + +msgid "Country Selection" +msgstr "国家/地区选择" + +msgid "Custom Feed Editor" +msgstr "自定义源编辑器" + +msgid "Deduplicate IP addresses across all active Sets and tidy up the local blocklist." +msgstr "去除所有活跃集合中的重复 IP 地址并整理本地黑名单。" + +msgid "Deduplicate IPs" +msgstr "IP 去重" + +msgid "Description" +msgstr "描述" + +msgid "Detect relevant network devices, interfaces, subnets, protocols and utilities automatically." +msgstr "自动检测相关的网络设备、接口、子网、协议和工具。" + +msgid "Disable" +msgstr "禁用" + +msgid "Don't check SSL server certificates during download." +msgstr "下载期间不检查 SSL 服务器证书。" + +msgid "Download" +msgstr "下载" + +msgid "Download Insecure" +msgstr "允许非安全下载" + +msgid "Download Parameters" +msgstr "下载参数" + +msgid "Download Retries" +msgstr "下载重试" + +msgid "Download Utility" +msgstr "下载工具" + +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "静默丢弃数据包或主动拒绝传入流量。" + +msgid "E-Mail Notification" +msgstr "邮件通知" + +msgid "E-Mail Profile" +msgstr "邮箱配置文件" + +msgid "E-Mail Receiver Address" +msgstr "电子邮件收件人地址" + +msgid "E-Mail Sender Address" +msgstr "电子邮件发件人地址" + +msgid "E-Mail Settings" +msgstr "电子邮件设置" + +msgid "E-Mail Topic" +msgstr "电子邮件主题" + +msgid "Edit Allowlist" +msgstr "编辑白名单" + +msgid "Edit Blocklist" +msgstr "编辑黑名单" + +msgid "Element Count" +msgstr "元素数量" + +msgid "Elements (max. 50)" +msgstr "元素(最多 50)" + +msgid "Empty field not allowed" +msgstr "不允许空的字段" + +msgid "Enable BCP38" +msgstr "启用 BCP38" + +msgid "Enable GeoIP Map" +msgstr "启用 GeoIP 地图" + +msgid "Enable NFT counters for Set elements and chain rules. Required for the GeoIP Map and packet statistics in the Set Reporting." +msgstr "对集合元素和链路规则启用 NFT 计数器。对 GeoIP 地图和集合报告中的数据包统计数字是必需的。" + +msgid "Enable Remote Logging" +msgstr "开启远程日志记录" + +msgid "Enable a GeoIP Map with suspicious Set elements. This requires external requests to get the map tiles and geolocation data." +msgstr "启用带可疑集合元素的 GeoIP 地图。这需要外部请求来获取地图磁贴和地理位置数据。" + +msgid "Enable the SecBox service." +msgstr "启用 SecBox 服务。" + +msgid "Enable the cgi interface to receive remote logging events." +msgstr "允许 cgi 接口接收远程日志记录事件。" + +msgid "Enable verbose debug logging in case of processing errors." +msgstr "如遇处理错误启用详细调试记录。" + +msgid "Enabled" +msgstr "已启用" + +msgid "Enables IPv4 support." +msgstr "启用 IPv4 支持。" + +msgid "Enables IPv6 support." +msgstr "启用 IPv6 支持。" + +msgid "Error Directory" +msgstr "错误目录" + +msgid "Expiry time for auto added blocklist Set members." +msgstr "自动添加的黑名单集成员的过期时间。" + +msgid "External Allowlist Feeds" +msgstr "外部白名单源" + +msgid "External Blocklist Feeds" +msgstr "外部黑名单源" + +msgid "Failed to generate a SecBox report!" +msgstr "生成 SecBox 报告失败!" + +msgid "Feed Complete" +msgstr "源完毕" + +msgid "Feed Flag Reset" +msgstr "源标记重置" + +msgid "Feed Name" +msgstr "源名称" + +msgid "Feed Selection" +msgstr "源选择" + +msgid "Feed/Set Settings" +msgstr "源/集合设置" + +msgid "Fill" +msgstr "补充/修改" + +msgid "Firewall Log" +msgstr "防火墙日志" + +msgid "Flag" +msgstr "标记" + +msgid "Flag not supported" +msgstr "此标记不受支持" + +msgid "General Settings" +msgstr "常规设置" + +msgid "Grant access to LuCI app SecBox" +msgstr "授予访问 LuCI 应用 SecBox 的权限" + +msgid "High Priority" +msgstr "高优先级" + +msgid "Highest Priority" +msgstr "最高优先级" + +msgid "ICMP-Threshold" +msgstr "ICMP 阈值" + +msgid "ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "ICMP 阈值用于防止 WAN DoS 攻击。要禁用此保护措施,将其设置为 '0'。" + +msgid "IP" +msgstr "IP" + +msgid "IP Search" +msgstr "IP 搜索" + +msgid "IP Search..." +msgstr "IP 搜索…" + +msgid "IPv4 Network Interfaces" +msgstr "IPv4 网络接口" + +msgid "IPv4 Support" +msgstr "IPv4 支持" + +msgid "IPv6 Network Interfaces" +msgstr "IPv6 网络接口" + +msgid "IPv6 Support" +msgstr "IPv6 支持" + +msgid "Inbound" +msgstr "传入" + +msgid "Inbound & Outbound" +msgstr "传入&传出" + +msgid "Inbound & Outbound Feed" +msgstr "传入&传出源" + +msgid "Inbound Block Policy" +msgstr "传入封禁策略" + +msgid "Inbound Feed" +msgstr "传入源" + +msgid "Inbound (packets)" +msgstr "传入 (数据包)" + +msgid "Increase the maximal number of open files, e.g. to handle the amount of temporary split files while loading the Sets." +msgstr "提升打开文件的最大数目来应对加载集合时的众多临时分割文件。" + +msgid "Information" +msgstr "信息" + +msgid "Invalid URL format" +msgstr "无效 URL 格式" + +msgid "Invalid characters" +msgstr "无效字符" + +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "无效的过期时间格式,如 5m, 2h, 1d 或 1h30m" + +msgid "Invalid input values, unable to save modifications." +msgstr "输入值无效,无法保存更改。" + +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - 服务拉丁美洲和加勒比地区" + +msgid "Last Run" +msgstr "最后运行" + +msgid "Least Priority" +msgstr "最低优先级" + +msgid "Less Priority" +msgstr "较低优先级" + +msgid "Limit the cpu cores used by SecBox to save RAM, autodetected by default." +msgstr "限制 SecBox 使用的 cpu 核心数来节省内存,默认自动检测。" + +msgid "Limit the uplink autoallow function." +msgstr "限制上行线路自动允许功能。" + +msgid "List of available network interfaces to trigger the SecBox start." +msgstr "触发 SecBox 启动的可用网络接口列表。" + +msgid "List the elements of a specific SecBox-related Set." +msgstr "列出与某一特定 SecBox 有关的集的元素。" + +msgid "Local Feed Settings" +msgstr "本地源设置" + +msgid "Location for parsing the log file, e.g. via syslog-ng, to deactivate the standard parsing via logread." +msgstr "通过 syslog-ng 解析日志文件来取消通过 logread 的标准解析的位置。" + +msgid "Log Burst Limit" +msgstr "记录 Burst 限制" + +msgid "Log Count" +msgstr "日志数" + +msgid "Log Inbound" +msgstr "记录传入数据" + +msgid "Log Limit" +msgstr "日志限制" + +msgid "Log Outbound" +msgstr "记录传出数据" + +msgid "Log Prerouting" +msgstr "记录预路由" + +msgid "Log Rate Limit" +msgstr "记录速率限制" + +msgid "Log Settings" +msgstr "日志设置" + +msgid "Log Terms" +msgstr "日志项" + +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "记录局域网转发链中的可疑数据包。" + +msgid "Log suspicious packets in the Prerouting chain." +msgstr "记录预路由链中的可疑数据包。" + +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "记录广域网输入和广域网转发链中的可疑数据包。" + +msgid "Logfile Location" +msgstr "日志文件位置" + +msgid "LuCI failed login" +msgstr "LuCI 登录失败" + +msgid "Map Reset" +msgstr "地图重置" + +msgid "Map..." +msgstr "地图…" + +msgid "Max Open Files" +msgstr "打开文件的最大数目" + +msgid "NFT Information" +msgstr "NFT 信息" + +msgid "NFT Log Level" +msgstr "NFT 日志级别" + +msgid "Network Devices" +msgstr "网络设备" + +msgid "Network error" +msgstr "网络错误" + +msgid "Nice Level" +msgstr "进程优先级" + +msgid "No %s related logs yet!" +msgstr "还没有 %s 相关的日志!" + +msgid "No GeoIP Map data!" +msgstr "没有 GeoIP 地图数据!" + +msgid "No SecBox config found!" +msgstr "未找到 SecBox 配置!" + +msgid "Normal Priority" +msgstr "正常优先级" + +msgid "Number of Set load attempts in case of an error." +msgstr "如果发生错误,集合加载尝试的数目。" + +msgid "Number of download attempts in case of an error (not supported by uclient-fetch)." +msgstr "下载出错时的重试次数(uclient-fetch 不支持)。" + +msgid "Number of failed login attempts of the same IP in the log before blocking." +msgstr "进行拦截前允许同一 IP 的失败登录尝试在日志中出现几次。" + +msgid "Opt out specific feeds from the deduplication process." +msgstr "将特定源排除在去重过程之外。" + +msgid "Outbound" +msgstr "传出" + +msgid "Outbound Feed" +msgstr "传出源" + +msgid "Outbound (packets)" +msgstr "传出 (数据包)" + +msgid "Override the default feed configuration and apply the feed to the inbound and outbound chain." +msgstr "覆盖默认源配置并将源应用到传入和传出链。" + +msgid "Override the default feed configuration and apply the feed to the inbound chain only." +msgstr "覆盖默认源配置并将源仅应用到传入链。" + +msgid "Override the default feed configuration and apply the feed to the outbound chain only." +msgstr "覆盖默认源配置并将源仅应用到传出链。" + +msgid "Override the default feed configuration and remove existing port/protocol limitations." +msgstr "覆盖默认源榜删除现有的端口/协议限制。" + +msgid "Override the pre-configured download options for the selected download utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the last parameter." +msgstr "覆盖所选下载工具的预配置下载选项。输出标记,如 curl 的 '-o' 或 wget 的 '-O',必须是最后一个参数。" + +msgid "Overview" +msgstr "概况" + +msgid "Parse only the last stated number of log entries for suspicious events. To disable the log monitor at all set it to '0'." +msgstr "仅解析可疑事件日志条目最后声明的数量。如需完全禁用日志监控,请将其设为 '0'。" + +msgid "Port / Protocol" +msgstr "端口 / 协议" + +msgid "Processing Log" +msgstr "处理日志" + +msgid "Profile used by 'msmtp' for SecBox notification E-Mails." +msgstr "“msmtp”所用的 SecBox 电子邮件通知配置。" + +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - 服务欧洲、中东和中亚" + +msgid "Rate (per second) for the shared NFT log limit, applied globally across all logged rules. Set to '0' to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers." +msgstr "共享的 NFT 日志限制的速率(每秒),全局应用于所有日志记录规则。当使用 ulogd 或其他用户空间日志处理器时设为 \"0\" 完全禁用速率限制。" + +msgid "Receive E-Mail notifications with every SecBox run." +msgstr "接收每次 SecBox 运行的电子邮件通知。" + +msgid "Receiver address for SecBox notification E-Mails, this information is required to enable E-Mail functionality." +msgstr "SecBox 通知电子邮件的接收地址,要开启电子邮件功能必须填写此信息。" + +msgid "Refresh" +msgstr "刷新" + +msgid "Regional Internet Registry" +msgstr "区域互联网注册系统" + +msgid "Regular expressions to detect suspicious IPs in the system log. Uses the last IP per line; prefix a term with 'first:' for source-first formats, e.g. nginx." +msgstr "在系统日志中检测可疑 IP 的正则表达式。每行使用最后一个 IP 地址;对于源优先格式,如 nginx,在术语前加上“first:” 前缀。" + +msgid "Remote Token" +msgstr "远端令牌" + +msgid "Remote logging Event" +msgstr "远程日志记录事件" + +msgid "Report Directory" +msgstr "抓包文件目录" + +msgid "Reporting Counters" +msgstr "报告计数" + +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "限制来自/对少量安全 IP 的互联网访问。" + +msgid "Result" +msgstr "结果" + +msgid "Rule" +msgstr "规则" + +msgid "Run Flags" +msgstr "运行标记" + +msgid "Run Information" +msgstr "运行信息" + +msgid "SYN-Threshold" +msgstr "SYN 阈值" + +msgid "SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "SYN 阈值用于防止 WAN DoS 攻击。要禁用此保护措施将其设置为 '0'。" + +msgid "Save" +msgstr "保存" + +msgid "Save & Reload" +msgstr "保存 & 重新加载" + +msgid "Save & Restart" +msgstr "保存并重启" + +msgid "Search IP" +msgstr "搜索 IP" + +msgid "Search is running, please wait..." +msgstr "搜索运行中,请等待…" + +msgid "Search the SecBox-related Sets for a specific IP." +msgstr "在 SecBox 相关的集中搜索一个特定的 IP。" + +msgid "Search timed out." +msgstr "搜索超时。" + +msgid "Select one of the pre-configured download utilities." +msgstr "选择一个预先配置的下载工具。" + +msgid "Select the WAN network device(s)." +msgstr "选择广域网网络设备。" + +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "选择逻辑广域网 IPv4 网络接口。" + +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "选择逻辑广域网 IPv6 网络接口。" + +msgid "Sender address for SecBox notification E-Mails." +msgstr "SecBox 通知邮件的发送地址。" + +msgid "Set" +msgstr "设置" + +msgid "Set Content" +msgstr "集合内容" + +msgid "Set Content..." +msgstr "集合内容…" + +msgid "Set Load Retries" +msgstr "集合加载重试" + +msgid "Set Policy" +msgstr "设置策略" + +msgid "Set Reporting" +msgstr "设置报告" + +msgid "Set Split Size" +msgstr "设置分割尺寸" + +msgid "Set details" +msgstr "集详情" + +msgid "Set the NFT chain priority within the SecBox table, lower values means higher priority." +msgstr "设置 SecBox 表格中的 NFT 链优先级,较低的值表示较高的优先级。" + +msgid "Set the NFT policy for SecBox-related Sets." +msgstr "设置 SecBox 相关集合的 NFT 策略。" + +msgid "Set the syslog level for NFT logging." +msgstr "设定 NFT 日志记录的 syslog 级别。" + +msgid "Settings" +msgstr "设置" + +msgid "Show Content" +msgstr "显示内容" + +msgid "Show only Set elements with hits" +msgstr "只显示有命中的集合元素" + +msgid "Split ASN Set" +msgstr "分割 ASN 集" + +msgid "Split Country Set" +msgstr "分割国家/地区集" + +msgid "Split external Set loading after every n members to save RAM, disabled by default." +msgstr "每 n 名成员后分割外部集合加载来节省内存,默认禁用。" + +msgid "Startup Trigger Interface" +msgstr "启动触发接口" + +msgid "Status" +msgstr "状态" + +msgid "Stop" +msgstr "停止" + +msgid "Subnet" +msgstr "子网" + +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "基于区域互联网注册机构(RIR)的地区概述。" + +msgid "System Info" +msgstr "系统信息" + +msgid "Table/Chain Settings" +msgstr "表/链设置" + +msgid "Target directory for SecBox-related error files." +msgstr "SecBox 相关错误文件的目标目录。" + +msgid "Target directory for SecBox-related report files." +msgstr "SecBox 相关报告文件的目标目录。" + +msgid "Target directory for compressed feed backups." +msgstr "压缩的源备份文件的目标目录。" + +msgid "The allowlist is too big, unable to save modifications." +msgstr "白名单太大,无法保存修改。" + +msgid "The blocklist is too big, unable to save modifications." +msgstr "黑名单太大,无法保存更改。" + +msgid "The selected ASNs are stored in separate Sets." +msgstr "所选的 ASN 被存储在不同的集合中。" + +msgid "The selected Countries are stored in separate Sets." +msgstr "所选国家/地区被存储在不同的集合中。" + +msgid "The selected priority will be used for SecBox background processing." +msgstr "所选的优先级将用于 SecBox 后台处理。" + +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "与 %s 相关的系统日志" + +msgid "This is the local SecBox allowlist that will permit certain MAC-, IP-addresses or domain names.
Please note: add only exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR notation and MAC/IP-bindings are allowed." +msgstr "这是本地 SecBox 白名单,会允许特定的 MAC、IP 地址或域名。
请注意: 每行只能添加一个 MAC/IPv4/IPv6 地址或域名。允许 CIDR 格式的 IP 范围和 MAC/IP 绑定。" + +msgid "This is the local SecBox blocklist that will prevent certain MAC-, IP-addresses or domain names.
Please note: add only exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR notation and MAC/IP-bindings are allowed." +msgstr "这是本地 SecBox 黑名单,会拦截特定的 MAC、IP 地址或域名。
请注意: 每行只能添加一个 MAC/IPv4/IPv6 地址或域名。允许 CIDR 格式的 IP 范围和 MAC/IP 绑定。" + +msgid "This report shows the latest NFT Set statistics, press the 'Refresh' button to get a new one. You can also display the specific content of Sets, search for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "此报告显示最新的 NFT 集合统计数字,请按下“刷新”按钮来获取新的报告。你也可以展示集合的特定内容、搜索可疑 IP;也可以在地图上展示这些 IP。" + +msgid "Timestamp" +msgstr "时间戳" + +msgid "To enable email notifications, set up the 'msmtp' package and specify a valid E-Mail receiver address." +msgstr "要启用电子邮件通知,设置 'msmtp' 包并指定有效的电子邮件接收者地址。" + +msgid "Token to communicate with the cgi interface." +msgstr "和 cgi 接口通信的令牌。" + +msgid "Topic for SecBox notification E-Mails." +msgstr "SecBox 通知邮件的主题。" + +msgid "Trigger Delay" +msgstr "触发延时" + +msgid "UDP-Threshold" +msgstr "UDP 阈值" + +msgid "UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "UDP 阈值用于防止 WAN DoS 攻击。要禁用此保护措施将其设置为 '0'。" + +msgid "URLv4" +msgstr "URLv4" + +msgid "URLv6" +msgstr "URLv6" + +msgid "Unable to parse the SecBox runtime information!" +msgstr "无法解析 SecBox 运行时信息!" + +msgid "Unable to parse the countries file!" +msgstr "无法解析国家文件!" + +msgid "Unable to parse the custom feed file!" +msgstr "无法解析自定义源文件!" + +msgid "Unable to parse the default feed file!" +msgstr "无法解析默认源文件!" + +msgid "Unable to parse the ruleset file!" +msgstr "无法解析规则集文件!" + +msgid "Unable to save modifications: %s" +msgstr "无法保存更改:%s" + +msgid "Upload" +msgstr "上传" + +msgid "Upload of the custom feed file failed." +msgstr "上传自定义源文件失败。" + +msgid "Verbose Debug Logging" +msgstr "详细的调试记录" + +msgid "With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). The file is located at '/etc/secbox/secbox.custom.feeds'. Then you can edit this file, delete entries, add new ones or make a local backup. To go back to the maintainers version just clear the custom feed file." +msgstr "使用此编辑器您可以上传本地自定义的源文件或填充一个初始文件(与软件包中提供的版本一模一样)。该文件位于 '/etc/secbox/secbox.custom.feeds'。然后,您可以编辑此文件删除条目添加新的条目或进行本地备份。要恢复到维护者版本只需清除自定义源文件即可。" + +msgid "alert" +msgstr "报警" + +msgid "asterisk invalid account" +msgstr "星号无效账号" + +msgid "auto-added IPs to allowlist" +msgstr "自动将 IP 添加到了白名单中" + +msgid "auto-added IPs to blocklist" +msgstr "自动将 IP 添加到了黑名单中" + +msgid "SecBox" +msgstr "网络安全" + +msgid "blocked bcp38 packets" +msgstr "拦截 bcp38 数据包" + +msgid "blocked icmp-flood packets" +msgstr "拦截了 icmp-flood 包" + +msgid "blocked invalid ct packets" +msgstr "拦截了无效的 ct 包" + +msgid "blocked invalid tcp packets" +msgstr "拦截了无效的 tcp 包" + +msgid "blocked syn-flood packets" +msgstr "拦截了 syn-flood 包" + +msgid "blocked udp-flood packets" +msgstr "拦截了 udp-flood 包" + +msgid "crit" +msgstr "关键项" + +msgid "debug" +msgstr "调试" + +msgid "drop" +msgstr "丢弃" + +msgid "dropbear failed login" +msgstr "dropbear 登录失败" + +msgid "emerg" +msgstr "紧急项" + +msgid "err" +msgstr "错误项" + +msgid "info" +msgstr "信息" + +msgid "local allowlist" +msgstr "本地白名单" + +msgid "local blocklist" +msgstr "本地黑名单" + +msgid "memory" +msgstr "内存" + +msgid "notice" +msgstr "通知" + +msgid "online documentation" +msgstr "在线文档" + +msgid "openvpn TLS error" +msgstr "openvpn TLS 错误" + +msgid "performance" +msgstr "性能" + +msgid "reject" +msgstr "拒绝" + +msgid "sshd closed connection" +msgstr "sshd 关闭了连接" + +msgid "sshd failed login" +msgstr "sshd 登录失败" + +msgid "warn" +msgstr "警告" + +msgid "The allowlist is too big, raw editor mode enforced." +msgstr "白名单过大,已强制使用纯文本编辑器模式。" + +msgid "System rule" +msgstr "系统规则" + +msgid "System Auto-Protected Rules" +msgstr "系统自动保护规则" + +msgid "rules" +msgstr "项规则" + +msgid "Auto detected by SecBox / protected" +msgstr "由 SecBox 自动检测 / 受保护" + +msgid "Delete this custom rule" +msgstr "删除此自定义规则" + +msgid "Delete" +msgstr "删除" + +msgid "Address / Domain / Network" +msgstr "地址 / 域名 / 网段" + +msgid "Comment" +msgstr "注释" + +msgid "Action" +msgstr "操作" + +msgid "No custom allowlist entries yet. Add one above." +msgstr "暂无自定义白名单条目,请在上方添加。" + +msgid "User Custom Rules" +msgstr "用户自定义规则" + +msgid "custom rules" +msgstr "项自定义规则" + +msgid "Switch to Raw Text Editor" +msgstr "切换到纯文本编辑器" + +msgid "e.g. 192.168.1.100, 10.0.0.0/24, or example.com" +msgstr "例如 192.168.1.100, 10.0.0.0/24 或 example.com" + +msgid "Optional comment (e.g. Office NAS)" +msgstr "可选注释(例如:办公室 NAS)" + +msgid "Please specify an IP, CIDR, Domain, or MAC address." +msgstr "请指定有效的 IP、CIDR、域名或 MAC 地址。" + +msgid "Invalid format: \"%s\" is not a valid IPv4, IPv6, CIDR, Domain or MAC." +msgstr "格式无效:\"%s\" 不是合法的 IPv4、IPv6、CIDR、域名或 MAC 地址。" + +msgid "Added custom rule \"%s\"." +msgstr "已添加自定义规则 \"%s\"。" + +msgid "Add User Custom Rule" +msgstr "添加用户自定义规则" + +msgid "Address or Domain" +msgstr "地址或域名" + +msgid "Comment / Note" +msgstr "注释 / 备注" + +msgid "+ Add Rule" +msgstr "+ 添加规则" + +msgid "Raw Text Editor" +msgstr "纯文本编辑器" + +msgid "Switch to Dual Table View" +msgstr "切换到双表格视图" + +msgid "One entry per line. Supports IPv4, IPv6, CIDR, MAC, Domain, and comments starting with \"#\"." +msgstr "每行一条记录。支持 IPv4、IPv6、CIDR、MAC、域名以及以 \"#\" 开头的注释。" + +msgid "SecBox Allowlist" +msgstr "网络安全白名单" + +msgid "Manage local trusted devices, subnets and domain names that will never be blocked by SecBox." +msgstr "管理受信任的本地设备、子网及域名,这些条目绝不会被 SecBox 拦截。" + +msgid "Reloading SecBox service..." +msgstr "正在重载 SecBox 服务..." + +msgid "Allowlist saved and SecBox service reloaded successfully!" +msgstr "白名单已保存,SecBox 服务重载成功!" + +msgid "Saved allowlist, but failed to reload SecBox: %s" +msgstr "已保存白名单,但重载 SecBox 失败:%s" + +msgid "Allowlist modifications have been saved." +msgstr "白名单修改已保存。" + +msgid "The blocklist is too big, raw editor mode enforced." +msgstr "黑名单过大,已强制使用纯文本编辑器模式。" + +msgid "Delete and unblock this rule" +msgstr "删除并解封此规则" + +msgid "Blocked Address / Domain / Network" +msgstr "拦截地址 / 域名 / 网段" + +msgid "Comment / Source" +msgstr "注释 / 来源" + +msgid "No blocked entries currently. Add one above." +msgstr "当前无黑名单拦截条目,请在上方添加。" + +msgid "Blocked Entries" +msgstr "黑名单拦截条目" + +msgid "active rules" +msgstr "项有效规则" + +msgid "e.g. 198.51.100.1, 203.0.113.0/24, or malicious-site.com" +msgstr "例如 198.51.100.1, 203.0.113.0/24 或 malicious-site.com" + +msgid "Optional comment (e.g. Malicious Bot)" +msgstr "可选注释(例如:恶意爬虫)" + +msgid "Added blocklist entry \"%s\"." +msgstr "已添加黑名单条目 \"%s\"。" + +msgid "Quick Add Blocklist Rule" +msgstr "快速添加黑名单规则" + +msgid "Switch to Table View" +msgstr "切换到表格视图" + +msgid "SecBox Blocklist" +msgstr "网络安全黑名单" + +msgid "Manage local blocked IP addresses, CIDR ranges, MAC addresses or domain names." +msgstr "管理本地拉黑的 IP 地址、CIDR 范围、MAC 地址或域名。" + +msgid "Blocklist saved and SecBox service reloaded successfully!" +msgstr "黑名单已保存,SecBox 服务重载成功!" + +msgid "Saved blocklist, but failed to reload SecBox: %s" +msgstr "已保存黑名单,但重载 SecBox 失败:%s" + +msgid "Blocklist modifications have been saved." +msgstr "黑名单修改已保存。" + +msgid "Executing action: %s..." +msgstr "正在执行操作:%s..." + +msgid "SecBox service action \"%s\" executed successfully." +msgstr "SecBox 服务操作 \"%s\" 执行成功。" + +msgid "Failed to execute action \"%s\": %s" +msgstr "执行操作 \"%s\" 失败:%s" + +msgid "Enabling and starting SecBox service..." +msgstr "正在启用并启动 SecBox 服务..." + +msgid "Stopping and disabling SecBox service..." +msgstr "正在停止并禁用 SecBox 服务..." + +msgid "SecBox service enabled and started." +msgstr "SecBox 服务已启用并成功启动。" + +msgid "SecBox service stopped and disabled." +msgstr "SecBox 服务已停止并成功禁用。" + +msgid "Failed to update service status: %s" +msgstr "更新服务状态失败:%s" + +msgid "Reload Service" +msgstr "重载服务" + +msgid "Restart Service" +msgstr "重启服务" + +msgid "Stop & Disable Service" +msgstr "停止并禁用服务" + +msgid "Start & Enable Service" +msgstr "启动并启用服务" + +msgid "SecBox Overview" +msgstr "网络安全概况" + +msgid "Real-time runtime status, active sets and blocking statistics for the SecBox service." +msgstr "SecBox 服务的实时运行状态、活动 IP 集及拦截统计。" + +msgid "Start Service" +msgstr "启动服务" + +msgid "Interfaces" +msgstr "接口信息" + +msgid "wan-dev" +msgstr "WAN 设备" + +msgid "wan-if" +msgstr "WAN 接口" + +msgid "vlan-allow" +msgstr "放行 VLAN" + +msgid "vlan-block" +msgstr "拦截 VLAN" + +msgid "uplink IPv4" +msgstr "上行 IPv4" + +msgid "uplink IPv6" +msgstr "上行 IPv6" + +msgid "Please enter or search an IP first." +msgstr "请先输入或搜索 IP 地址。" + +msgid "IP \"%s\" is already in %s." +msgstr "IP \"%s\" 已存在于 %s 中。" + +msgid "Successfully added \"%s\" to %s!" +msgstr "成功将 \"%s\" 添加到 %s!" + +msgid "Failed to update list: %s" +msgstr "更新列表失败:%s" + +msgid "IP Search & Quick Action" +msgstr "IP 搜索与快速操作" + +msgid "Search the SecBox-related Sets for a specific IP and take quick actions." +msgstr "在 SecBox 关联集合中检索指定 IP 并执行快捷操作。" + +msgid "Allowlist" +msgstr "白名单" + +msgid "+ Add to Allowlist" +msgstr "+ 加入白名单" + +msgid "Blocklist" +msgstr "黑名单" + +msgid "+ Add to Blocklist" +msgstr "+ 加入黑名单" + +msgid "Close" +msgstr "关闭" + +msgid "packet counters disabled" +msgstr "数据包计数器已禁用" + +msgid "SecBox Set Reporting" +msgstr "网络安全集合报表" + +msgid "Sets" +msgstr "IP 集合" + +msgid "Top Sets" +msgstr "命中最多集合" + +msgid "elements / packets" +msgstr "条目 / 数据包" + +msgid "no hits yet" +msgstr "暂无命中" + +msgid "Worst Sets" +msgstr "拦截最多集合" + +msgid "Blocked Packets" +msgstr "已拦截数据包" + +msgid "syn-flood" +msgstr "SYN 洪水" + +msgid "udp-flood" +msgstr "UDP 洪水" + +msgid "icmp-flood" +msgstr "ICMP 洪水" + +msgid "invalid ct" +msgstr "无效连接" + +msgid "invalid tcp" +msgstr "无效 TCP" + +msgid "bcp38" +msgstr "BCP38 过滤" + +msgid "Auto-added IPs" +msgstr "自动添加的 IP" + +msgid "allowlist" +msgstr "白名单" + +msgid "blocklist" +msgstr "黑名单" + +msgid "Applying configuration & reloading SecBox..." +msgstr "正在应用配置并重载 SecBox..." + +msgid "SecBox configuration saved and service %sed successfully." +msgstr "SecBox 配置已保存,服务已成功 %s。" + +msgid "Failed to %s SecBox: %s" +msgstr "%s SecBox 失败:%s" + +msgid "SecBox Settings" +msgstr "网络安全设置" + +msgid "Changes on this tab need a SecBox service restart to take effect." +msgstr "此标签页上的更改需要重启 SecBox 服务后生效。" + +msgid "Detect relevant network devices, interfaces, subnets, protocols and utilities automatically." +msgstr "自动检测相关的网络设备、接口、子网、协议和实用工具。" + +msgid "Override the pre-configured download options for the selected download utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the last parameter." +msgstr "覆盖所选下载工具的预配置下载参数。输出参数(如 curl 的 '-o' 或 wget 的 '-O')必须作为最后一个参数。" + +msgid "Number of download attempts in case of an error (not supported by uclient-fetch)." +msgstr "下载出错时的重试次数(uclient-fetch 不支持)。" + +msgid "Enable NFT counters for Set elements and chain rules. Required for the GeoIP Map and packet statistics in the Set Reporting." +msgstr "为集合条目和链规则启用 NFT 计数器。GeoIP 地图和集合报表统计需要此选项。" + +msgid "Enable a GeoIP Map with suspicious Set elements. This requires external requests to get the map tiles and geolocation data." +msgstr "启用可疑集合条目的 GeoIP 地图。这需要向外部发起请求以获取地图瓦片和地理位置数据。" + +msgid "Increase the maximal number of open files, e.g. to handle the amount of temporary split files while loading the Sets." +msgstr "增加最大打开文件数限制,例如在加载集合时处理大量临时分割文件。" + +msgid "Split external Set loading after every n members to save RAM, disabled by default." +msgstr "每隔 n 个成员分割外部集合加载以节省内存,默认禁用。" + +msgid "Deduplicate IP addresses across all active Sets and tidy up the local blocklist." +msgstr "在所有活动集合中对 IP 地址进行去重并整理本地黑名单。" + +msgid "Set the NFT chain priority within the SecBox table, lower values means higher priority." +msgstr "设置 SecBox 表内 NFT 链的优先级,数值越低优先级越高。" + +msgid "Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-Input and WAN-Forward chain." +msgstr "在 WAN-Input 和 WAN-Forward 链中始终放行带有特定端口或端口范围的协议 (tcp/udp)。" + +msgid "ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "每秒 ICMP 阈值包数,以防止 WAN-DoS 攻击。设为 '0' 可禁用此安全防护。" + +msgid "SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "每秒 SYN 阈值包数,以防止 WAN-DoS 攻击。设为 '0' 可禁用此安全防护。" + +msgid "UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "每秒 UDP 阈值包数,以防止 WAN-DoS 攻击。设为 '0' 可禁用此安全防护。" + +msgid "Override the default feed configuration and apply the feed to the inbound chain only." +msgstr "覆盖默认源配置,仅将该源应用于入站链(Inbound)。" + +msgid "Override the default feed configuration and apply the feed to the outbound chain only." +msgstr "覆盖默认源配置,仅将该源应用于出站链(Outbound)。" + +msgid "Override the default feed configuration and apply the feed to the inbound and outbound chain." +msgstr "覆盖默认源配置,将该源同时应用于入站与出站链。" + +msgid "Override the default feed configuration and remove existing port/protocol limitations." +msgstr "覆盖默认源配置并移除现有的端口/协议限制。" + +msgid "Location for parsing the log file, e.g. via syslog-ng, to deactivate the standard parsing via logread." +msgstr "解析日志文件的路径(例如通过 syslog-ng),以停用通过 logread 的标准解析。" + +msgid "Parse only the last stated number of log entries for suspicious events. To disable the log monitor at all set it to '0'." +msgstr "仅解析最后指定数量的日志条目以查找可疑事件。设为 '0' 可完全禁用日志监视器。" + +msgid "Number of failed login attempts of the same IP in the log before blocking." +msgstr "在日志中同一 IP 登录失败多少次后触发拉黑拦截。" + +msgid "Rate (per second) for the shared NFT log limit, applied globally across all logged rules. Set to '0' to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers." +msgstr "全局 NFT 日志记录速率限制(每秒条数)。设为 '0' 可完全禁用速率限制(例如使用 ulogd 时)。" + +msgid "Regular expressions to detect suspicious IPs in the system log. Uses the last IP per line; prefix a term with 'first:' for source-first formats, e.g. nginx." +msgstr "用于在系统日志中检测可疑 IP 的正则表达式。默认使用每行的最后一个 IP;前缀加 'first:' 可匹配源优先格式(如 nginx)。" + +msgid "To enable email notifications, set up the 'msmtp' package and specify a valid E-Mail receiver address." +msgstr "要启用邮件通知,请配置 'msmtp' 软件包并指定有效的收件人邮箱地址。" + +msgid "Receiver address for SecBox notification E-Mails, this information is required to enable E-Mail functionality." +msgstr "接收 SecBox 告警邮件的收件人地址,必须填写此项才能启用邮件功能。" + +msgid "Automatically add resolved domains and uplink IPs to the local SecBox allowlist." +msgstr "自动将解析出的域名及上行 IP 添加到本地 SecBox 白名单。" + +msgid "Automatically add resolved domains and suspicious IPs to the local SecBox blocklist." +msgstr "自动将解析出的域名及可疑 IP 添加到本地 SecBox 黑名单。" + +msgid "Automatically add entire subnets to the blocklist Set based on an additional RDAP request with the suspicious IP." +msgstr "基于可疑 IP 的 RDAP 查询结果,自动将整个所属子网添加到黑名单集合中。" + +msgid "Stop Service" +msgstr "停止服务" + +msgid "Table" +msgstr "表" + +msgid "Priority" +msgstr "优先级" + +msgid "Policy" +msgstr "策略" + +msgid "IPv4 Protocol" +msgstr "IPv4 协议" + +msgid "IPv6 Protocol" +msgstr "IPv6 协议" + +msgid "ICMP Limit" +msgstr "ICMP 限制" + +msgid "SYN Limit" +msgstr "SYN 限制" + +msgid "UDP Limit" +msgstr "UDP 限制" + +msgid "Log Service" +msgstr "日志服务" + +msgid "Download Tool" +msgstr "下载工具" + +msgid "WAN Device" +msgstr "WAN 设备" + +msgid "WAN Interface" +msgstr "WAN 接口" + +msgid "Allowed VLAN" +msgstr "放行 VLAN" + +msgid "Blocked VLAN" +msgstr "拦截 VLAN" + +msgid "Uplink IPv4" +msgstr "上行 IPv4" + +msgid "Uplink IPv6" +msgstr "上行 IPv6" + +msgid "Running (active)" +msgstr "运行中 (active)" + +msgid "Processing..." +msgstr "处理中..." + +msgid "Disabled" +msgstr "已禁用" + +msgid "Error" +msgstr "运行异常" + +msgid "Stopped" +msgstr "已停止" + +msgid "Not Running" +msgstr "未运行" + +msgid "Rule Elements" +msgstr "规则条目数" + +msgid "Configuration of the SecBox package to ban incoming and outgoing IPs via named nftables Sets. For further information please check the %s." +msgstr "SecBox 软件包配置,通过指定的 nftables 集合拦截入站和出站 IP。更多详细信息请参阅 %s。" + + +msgid "Live Log Console" +msgstr "实时日志控制台" + +msgid "Auto-scroll: ON" +msgstr "自动滚动: 开" + +msgid "Auto-scroll: OFF" +msgstr "自动滚动: 关" + +msgid "System Log Output" +msgstr "系统日志输出" + +msgid "Live Log Output" +msgstr "实时日志输出" + +msgid "No related logs" +msgstr "没有相关日志" + +msgid "Log Output" +msgstr "日志输出" + +msgid "Blocklist Entries" +msgstr "黑名单条目" + +msgid "Quick Add Block Rule" +msgstr "添加黑名单规则" + +msgid "Service Status" +msgstr "服务状态" + +msgid "NFT & Interface Information" +msgstr "NFT 与网络接口" + +msgid "Active Feeds & Flags" +msgstr "活动源与运行标志" + +msgid "Runtime Information" +msgstr "运行详细参数" + +msgid "Set Overview" +msgstr "集合概况" + +msgid "Blocked Packets & Auto-added IPs" +msgstr "攻击拦截与自动添加" + +msgid "Top Sets (Most Hits)" +msgstr "命中最多集合" + +msgid "Auto-added to Allowlist" +msgstr "自动添加至白名单" + +msgid "Auto-added to Blocklist" +msgstr "自动添加至黑名单" + +msgid "Hits" +msgstr "命中数" + +msgid "Top Sets (Most Hits)" +msgstr "命中最多集合" + +msgid "Worst Sets (Least Hits)" +msgstr "最少命中集合" + +msgid "Elements" +msgstr "规则条目" + +msgid "No custom allowlist entries yet. Add one below." +msgstr "暂无自定义白名单条目,请在下方添加。" + +msgid "Please specify a valid IP, CIDR, Domain or MAC address." +msgstr "请输入有效的 IP、CIDR 网段、域名或 MAC 地址。" + +msgid "Format invalid: \"%s\" is not a valid address without spaces." +msgstr "格式无效:“%s” 不是有效的无空格地址。" + +msgid "Add Rule" +msgstr "添加规则" + +msgid "One record per line. Supports IPv4, IPv6, CIDR, MAC, Domain and comments starting with \"#\"." +msgstr "每行一条记录。支持 IPv4、IPv6、CIDR 网段、MAC、域名以及以 “#” 开头的注释。" + +msgid "Manage trusted local devices, subnets and domains that will never be blocked by SecBox." +msgstr "管理受信任的本地设备、子网及域名,这些目标将永不被网络安全服务拦截。" + +msgid "Allowlist changes saved." +msgstr "白名单更改已成功保存。" + +msgid "Unable to save changes: %s" +msgstr "保存更改失败:%s" + +msgid "Currently no blocked entries. Add one below." +msgstr "暂无拦截条目,请在下方添加。" + +msgid "Manage locally blocked IP addresses, CIDR ranges, MAC addresses or domains." +msgstr "管理本地拦截的 IP 地址、CIDR 网段、MAC 地址或域名。" + +msgid "Blocklist changes saved." +msgstr "黑名单更改已成功保存。" + +msgid "Protocol/URL format not supported" +msgstr "不支持的协议/URL 格式" + +msgid "Version" +msgstr "版本" + +msgid "Rule Elements & Last Run" +msgstr "规则条目与最后运行" + +msgid "网络安全" +msgstr "网络安全" + +msgid "概况" +msgstr "概况" + +msgid "配置" +msgstr "配置" + +msgid "编辑白名单" +msgstr "编辑白名单" + +msgid "编辑黑名单" +msgstr "编辑黑名单" + +msgid "自定义源编辑器" +msgstr "自定义源编辑器" + +msgid "设置报告" +msgstr "设置报告" + +msgid "防火墙日志" +msgstr "防火墙日志" + +msgid "处理日志" +msgstr "处理日志" + +msgid "SecBox Configuration" +msgstr "网络安全配置" + +msgid "SecBox Custom Feed Editor" +msgstr "自定义源编辑器" + +msgid "SecBox Firewall Log" +msgstr "防火墙日志" + +msgid "SecBox Processing Log" +msgstr "处理日志" + +msgid "With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). The file is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, delete entries, add new ones or make a local backup. To go back to the maintainers version just clear the custom feed file." +msgstr "通过此编辑器,您可以上传本地自定义黑名单源文件或初始化一个初始文件(与软件包附带版本 1:1 完全一致的副本)。该文件位于 “/etc/banip/banip.custom.feeds”。随后您可以自由编辑该文件、删除条目、添加新源或创建本地备份。若要恢复为系统默认版本,只需清空自定义源文件即可。" + +msgid "Configuration" +msgstr "配置" + +msgid "Custom Feeds" +msgstr "自定义源编辑器" + +msgid "Running" +msgstr "运行中" diff --git a/luci-app-secbox/po/zh_Hans/secbox.po b/luci-app-secbox/po/zh_Hans/secbox.po new file mode 100644 index 000000000..be5e0343b --- /dev/null +++ b/luci-app-secbox/po/zh_Hans/secbox.po @@ -0,0 +1,1524 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: zh_Hans\n" + +msgid "-- Please choose (optional) --" +msgstr "-- 请选择(可选) --" + +msgid "-- Set Selection --" +msgstr "-- IP 集选择 --" + +msgid "-- default --" +msgstr "-- 默认 --" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "" +msgstr "" + +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - 服务非洲和印度洋区域" + +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - 服务亚太区域" + +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - 服务加拿大和美国" + +msgid "ASN Selection" +msgstr "ASN 选择" + +msgid "ASNs" +msgstr "平均取样数" + +msgid "Active Devices" +msgstr "活动设备" + +msgid "Active Feeds" +msgstr "活动源" + +msgid "Active Uplink" +msgstr "活跃的上行线路" + +msgid "AdGuardHome login error" +msgstr "AdGuardHome 登录错误" + +msgid "Additional trigger delay in seconds before SecBox processing begins." +msgstr "SecBox 处理开始之前的额外触发延迟(以秒为单位)。" + +msgid "Advanced Settings" +msgstr "高级设置" + +msgid "Allow Protocol/Ports" +msgstr "允许协议/端口" + +msgid "Allow VLAN Forwards" +msgstr "允许 VLAN 转发" + +msgid "Allowlist Feed URLs" +msgstr "白名单源 URL" + +msgid "Allowlist Only" +msgstr "仅白名单" + +msgid "Allowlist modifications have been saved, reload SecBox that changes take effect." +msgstr "允许列表的修改已保存,重新加载 SecBox 以使更改生效。" + +msgid "Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-Input and WAN-Forward chain." +msgstr "在 WAN-Input 和 Wan-Forward 链中始终允许协议(tcp/udp)特定端口或端口区间。" + +msgid "Always allow certain VLAN forwards." +msgstr "始终允许特定的 VLAN 转发。" + +msgid "Always block certain VLAN forwards." +msgstr "始终拦截特定的 VLAN 转发。" + +msgid "Auto Allow Uplink" +msgstr "自动允许上行线路" + +msgid "Auto Allowlist" +msgstr "自动白名单" + +msgid "Auto Block Subnet" +msgstr "自动拦截子网" + +msgid "Auto Blocklist" +msgstr "自动黑名单" + +msgid "Auto Detection" +msgstr "自动检测" + +msgid "Automatically add entire subnets to the blocklist Set based on an additional RDAP request with the suspicious IP." +msgstr "基于和可疑 IP 地址的额外 RDAP 请求自动添加条目子网到黑名单集。" + +msgid "Automatically add resolved domains and suspicious IPs to the local SecBox blocklist." +msgstr "自动添加已解析域名和可疑 IP 到本地 SecBox 黑名单。" + +msgid "Automatically add resolved domains and uplink IPs to the local SecBox allowlist." +msgstr "自动添加已解析域名和上行线路 IP 到本地 SecBox 白名单。" + +msgid "Backup Directory" +msgstr "备份目录" + +msgid "Base Directory" +msgstr "工作目录" + +msgid "Base working directory while SecBox processing." +msgstr "SecBox 处理时的基础工作目录。" + +msgid "Block VLAN Forwards" +msgstr "拦截 VLAN 转发" + +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "在所有支持的链中拦截伪造源 IP 地址的数据包。" + +msgid "Blocklist Feed" +msgstr "黑名单源" + +msgid "Blocklist Set Expiry" +msgstr "黑名单集过期时间" + +msgid "Blocklist modifications have been saved, reload SecBox that changes take effect." +msgstr "阻止列表的修改已保存, 重新加载 SecBox 以使更改生效。" + +msgid "Burst size in packets for the shared NFT log limit." +msgstr "共享 NFT 日志限制的 Burst 数据包大小。" + +msgid "CPU Cores" +msgstr "CPU 核心数" + +msgid "Cancel" +msgstr "取消" + +msgid "Chain" +msgstr "链" + +msgid "Chain Priority" +msgstr "链优先级" + +msgid "Changes on this tab needs a SecBox service reload to take effect." +msgstr "此标签页上的更改需要重新加载 SecBox 服务方能生效。" + +msgid "Changes on this tab needs a SecBox service restart to take effect." +msgstr "此标签页上进行的更改需要重启 SecBox 服务才能生效。" + +msgid "Clear" +msgstr "清除" + +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "基于 ASN 号码的 IP 地址集。" + +msgid "Configuration of the SecBox package to ban incoming and outgoing IPs via named nftables Sets. For further information please check the %s." +msgstr "配置 SecBox 包通过命名的 nftables 集封禁入站和出站 IP。进一步信息请见 %s。" + +msgid "Count" +msgstr "计数" + +msgid "Countries" +msgstr "国家或地区" + +msgid "Country Selection" +msgstr "国家/地区选择" + +msgid "Custom Feed Editor" +msgstr "自定义源编辑器" + +msgid "Deduplicate IP addresses across all active Sets and tidy up the local blocklist." +msgstr "去除所有活跃集合中的重复 IP 地址并整理本地黑名单。" + +msgid "Deduplicate IPs" +msgstr "IP 去重" + +msgid "Description" +msgstr "描述" + +msgid "Detect relevant network devices, interfaces, subnets, protocols and utilities automatically." +msgstr "自动检测相关的网络设备、接口、子网、协议和工具。" + +msgid "Disable" +msgstr "禁用" + +msgid "Don't check SSL server certificates during download." +msgstr "下载期间不检查 SSL 服务器证书。" + +msgid "Download" +msgstr "下载" + +msgid "Download Insecure" +msgstr "允许非安全下载" + +msgid "Download Parameters" +msgstr "下载参数" + +msgid "Download Retries" +msgstr "下载重试" + +msgid "Download Utility" +msgstr "下载工具" + +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "静默丢弃数据包或主动拒绝传入流量。" + +msgid "E-Mail Notification" +msgstr "邮件通知" + +msgid "E-Mail Profile" +msgstr "邮箱配置文件" + +msgid "E-Mail Receiver Address" +msgstr "电子邮件收件人地址" + +msgid "E-Mail Sender Address" +msgstr "电子邮件发件人地址" + +msgid "E-Mail Settings" +msgstr "电子邮件设置" + +msgid "E-Mail Topic" +msgstr "电子邮件主题" + +msgid "Edit Allowlist" +msgstr "编辑白名单" + +msgid "Edit Blocklist" +msgstr "编辑黑名单" + +msgid "Element Count" +msgstr "元素数量" + +msgid "Elements (max. 50)" +msgstr "元素(最多 50)" + +msgid "Empty field not allowed" +msgstr "不允许空的字段" + +msgid "Enable BCP38" +msgstr "启用 BCP38" + +msgid "Enable GeoIP Map" +msgstr "启用 GeoIP 地图" + +msgid "Enable NFT counters for Set elements and chain rules. Required for the GeoIP Map and packet statistics in the Set Reporting." +msgstr "对集合元素和链路规则启用 NFT 计数器。对 GeoIP 地图和集合报告中的数据包统计数字是必需的。" + +msgid "Enable Remote Logging" +msgstr "开启远程日志记录" + +msgid "Enable a GeoIP Map with suspicious Set elements. This requires external requests to get the map tiles and geolocation data." +msgstr "启用带可疑集合元素的 GeoIP 地图。这需要外部请求来获取地图磁贴和地理位置数据。" + +msgid "Enable the SecBox service." +msgstr "启用 SecBox 服务。" + +msgid "Enable the cgi interface to receive remote logging events." +msgstr "允许 cgi 接口接收远程日志记录事件。" + +msgid "Enable verbose debug logging in case of processing errors." +msgstr "如遇处理错误启用详细调试记录。" + +msgid "Enabled" +msgstr "已启用" + +msgid "Enables IPv4 support." +msgstr "启用 IPv4 支持。" + +msgid "Enables IPv6 support." +msgstr "启用 IPv6 支持。" + +msgid "Error Directory" +msgstr "错误目录" + +msgid "Expiry time for auto added blocklist Set members." +msgstr "自动添加的黑名单集成员的过期时间。" + +msgid "External Allowlist Feeds" +msgstr "外部白名单源" + +msgid "External Blocklist Feeds" +msgstr "外部黑名单源" + +msgid "Failed to generate a SecBox report!" +msgstr "生成 SecBox 报告失败!" + +msgid "Feed Complete" +msgstr "源完毕" + +msgid "Feed Flag Reset" +msgstr "源标记重置" + +msgid "Feed Name" +msgstr "源名称" + +msgid "Feed Selection" +msgstr "源选择" + +msgid "Feed/Set Settings" +msgstr "源/集合设置" + +msgid "Fill" +msgstr "补充/修改" + +msgid "Firewall Log" +msgstr "防火墙日志" + +msgid "Flag" +msgstr "标记" + +msgid "Flag not supported" +msgstr "此标记不受支持" + +msgid "General Settings" +msgstr "常规设置" + +msgid "Grant access to LuCI app SecBox" +msgstr "授予访问 LuCI 应用 SecBox 的权限" + +msgid "High Priority" +msgstr "高优先级" + +msgid "Highest Priority" +msgstr "最高优先级" + +msgid "ICMP-Threshold" +msgstr "ICMP 阈值" + +msgid "ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "ICMP 阈值用于防止 WAN DoS 攻击。要禁用此保护措施,将其设置为 '0'。" + +msgid "IP" +msgstr "IP" + +msgid "IP Search" +msgstr "IP 搜索" + +msgid "IP Search..." +msgstr "IP 搜索…" + +msgid "IPv4 Network Interfaces" +msgstr "IPv4 网络接口" + +msgid "IPv4 Support" +msgstr "IPv4 支持" + +msgid "IPv6 Network Interfaces" +msgstr "IPv6 网络接口" + +msgid "IPv6 Support" +msgstr "IPv6 支持" + +msgid "Inbound" +msgstr "传入" + +msgid "Inbound & Outbound" +msgstr "传入&传出" + +msgid "Inbound & Outbound Feed" +msgstr "传入&传出源" + +msgid "Inbound Block Policy" +msgstr "传入封禁策略" + +msgid "Inbound Feed" +msgstr "传入源" + +msgid "Inbound (packets)" +msgstr "传入 (数据包)" + +msgid "Increase the maximal number of open files, e.g. to handle the amount of temporary split files while loading the Sets." +msgstr "提升打开文件的最大数目来应对加载集合时的众多临时分割文件。" + +msgid "Information" +msgstr "信息" + +msgid "Invalid URL format" +msgstr "无效 URL 格式" + +msgid "Invalid characters" +msgstr "无效字符" + +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "无效的过期时间格式,如 5m, 2h, 1d 或 1h30m" + +msgid "Invalid input values, unable to save modifications." +msgstr "输入值无效,无法保存更改。" + +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - 服务拉丁美洲和加勒比地区" + +msgid "Last Run" +msgstr "最后运行" + +msgid "Least Priority" +msgstr "最低优先级" + +msgid "Less Priority" +msgstr "较低优先级" + +msgid "Limit the cpu cores used by SecBox to save RAM, autodetected by default." +msgstr "限制 SecBox 使用的 cpu 核心数来节省内存,默认自动检测。" + +msgid "Limit the uplink autoallow function." +msgstr "限制上行线路自动允许功能。" + +msgid "List of available network interfaces to trigger the SecBox start." +msgstr "触发 SecBox 启动的可用网络接口列表。" + +msgid "List the elements of a specific SecBox-related Set." +msgstr "列出与某一特定 SecBox 有关的集的元素。" + +msgid "Local Feed Settings" +msgstr "本地源设置" + +msgid "Location for parsing the log file, e.g. via syslog-ng, to deactivate the standard parsing via logread." +msgstr "通过 syslog-ng 解析日志文件来取消通过 logread 的标准解析的位置。" + +msgid "Log Burst Limit" +msgstr "记录 Burst 限制" + +msgid "Log Count" +msgstr "日志数" + +msgid "Log Inbound" +msgstr "记录传入数据" + +msgid "Log Limit" +msgstr "日志限制" + +msgid "Log Outbound" +msgstr "记录传出数据" + +msgid "Log Prerouting" +msgstr "记录预路由" + +msgid "Log Rate Limit" +msgstr "记录速率限制" + +msgid "Log Settings" +msgstr "日志设置" + +msgid "Log Terms" +msgstr "日志项" + +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "记录局域网转发链中的可疑数据包。" + +msgid "Log suspicious packets in the Prerouting chain." +msgstr "记录预路由链中的可疑数据包。" + +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "记录广域网输入和广域网转发链中的可疑数据包。" + +msgid "Logfile Location" +msgstr "日志文件位置" + +msgid "LuCI failed login" +msgstr "LuCI 登录失败" + +msgid "Map Reset" +msgstr "地图重置" + +msgid "Map..." +msgstr "地图…" + +msgid "Max Open Files" +msgstr "打开文件的最大数目" + +msgid "NFT Information" +msgstr "NFT 信息" + +msgid "NFT Log Level" +msgstr "NFT 日志级别" + +msgid "Network Devices" +msgstr "网络设备" + +msgid "Network error" +msgstr "网络错误" + +msgid "Nice Level" +msgstr "进程优先级" + +msgid "No %s related logs yet!" +msgstr "还没有 %s 相关的日志!" + +msgid "No GeoIP Map data!" +msgstr "没有 GeoIP 地图数据!" + +msgid "No SecBox config found!" +msgstr "未找到 SecBox 配置!" + +msgid "Normal Priority" +msgstr "正常优先级" + +msgid "Number of Set load attempts in case of an error." +msgstr "如果发生错误,集合加载尝试的数目。" + +msgid "Number of download attempts in case of an error (not supported by uclient-fetch)." +msgstr "下载出错时的重试次数(uclient-fetch 不支持)。" + +msgid "Number of failed login attempts of the same IP in the log before blocking." +msgstr "进行拦截前允许同一 IP 的失败登录尝试在日志中出现几次。" + +msgid "Opt out specific feeds from the deduplication process." +msgstr "将特定源排除在去重过程之外。" + +msgid "Outbound" +msgstr "传出" + +msgid "Outbound Feed" +msgstr "传出源" + +msgid "Outbound (packets)" +msgstr "传出 (数据包)" + +msgid "Override the default feed configuration and apply the feed to the inbound and outbound chain." +msgstr "覆盖默认源配置并将源应用到传入和传出链。" + +msgid "Override the default feed configuration and apply the feed to the inbound chain only." +msgstr "覆盖默认源配置并将源仅应用到传入链。" + +msgid "Override the default feed configuration and apply the feed to the outbound chain only." +msgstr "覆盖默认源配置并将源仅应用到传出链。" + +msgid "Override the default feed configuration and remove existing port/protocol limitations." +msgstr "覆盖默认源榜删除现有的端口/协议限制。" + +msgid "Override the pre-configured download options for the selected download utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the last parameter." +msgstr "覆盖所选下载工具的预配置下载选项。输出标记,如 curl 的 '-o' 或 wget 的 '-O',必须是最后一个参数。" + +msgid "Overview" +msgstr "概况" + +msgid "Parse only the last stated number of log entries for suspicious events. To disable the log monitor at all set it to '0'." +msgstr "仅解析可疑事件日志条目最后声明的数量。如需完全禁用日志监控,请将其设为 '0'。" + +msgid "Port / Protocol" +msgstr "端口 / 协议" + +msgid "Processing Log" +msgstr "处理日志" + +msgid "Profile used by 'msmtp' for SecBox notification E-Mails." +msgstr "“msmtp”所用的 SecBox 电子邮件通知配置。" + +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - 服务欧洲、中东和中亚" + +msgid "Rate (per second) for the shared NFT log limit, applied globally across all logged rules. Set to '0' to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers." +msgstr "共享的 NFT 日志限制的速率(每秒),全局应用于所有日志记录规则。当使用 ulogd 或其他用户空间日志处理器时设为 \"0\" 完全禁用速率限制。" + +msgid "Receive E-Mail notifications with every SecBox run." +msgstr "接收每次 SecBox 运行的电子邮件通知。" + +msgid "Receiver address for SecBox notification E-Mails, this information is required to enable E-Mail functionality." +msgstr "SecBox 通知电子邮件的接收地址,要开启电子邮件功能必须填写此信息。" + +msgid "Refresh" +msgstr "刷新" + +msgid "Regional Internet Registry" +msgstr "区域互联网注册系统" + +msgid "Regular expressions to detect suspicious IPs in the system log. Uses the last IP per line; prefix a term with 'first:' for source-first formats, e.g. nginx." +msgstr "在系统日志中检测可疑 IP 的正则表达式。每行使用最后一个 IP 地址;对于源优先格式,如 nginx,在术语前加上“first:” 前缀。" + +msgid "Remote Token" +msgstr "远端令牌" + +msgid "Remote logging Event" +msgstr "远程日志记录事件" + +msgid "Report Directory" +msgstr "抓包文件目录" + +msgid "Reporting Counters" +msgstr "报告计数" + +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "限制来自/对少量安全 IP 的互联网访问。" + +msgid "Result" +msgstr "结果" + +msgid "Rule" +msgstr "规则" + +msgid "Run Flags" +msgstr "运行标记" + +msgid "Run Information" +msgstr "运行信息" + +msgid "SYN-Threshold" +msgstr "SYN 阈值" + +msgid "SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "SYN 阈值用于防止 WAN DoS 攻击。要禁用此保护措施将其设置为 '0'。" + +msgid "Save" +msgstr "保存" + +msgid "Save & Reload" +msgstr "保存 & 重新加载" + +msgid "Save & Restart" +msgstr "保存并重启" + +msgid "Search IP" +msgstr "搜索 IP" + +msgid "Search is running, please wait..." +msgstr "搜索运行中,请等待…" + +msgid "Search the SecBox-related Sets for a specific IP." +msgstr "在 SecBox 相关的集中搜索一个特定的 IP。" + +msgid "Search timed out." +msgstr "搜索超时。" + +msgid "Select one of the pre-configured download utilities." +msgstr "选择一个预先配置的下载工具。" + +msgid "Select the WAN network device(s)." +msgstr "选择广域网网络设备。" + +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "选择逻辑广域网 IPv4 网络接口。" + +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "选择逻辑广域网 IPv6 网络接口。" + +msgid "Sender address for SecBox notification E-Mails." +msgstr "SecBox 通知邮件的发送地址。" + +msgid "Set" +msgstr "设置" + +msgid "Set Content" +msgstr "集合内容" + +msgid "Set Content..." +msgstr "集合内容…" + +msgid "Set Load Retries" +msgstr "集合加载重试" + +msgid "Set Policy" +msgstr "设置策略" + +msgid "Set Reporting" +msgstr "设置报告" + +msgid "Set Split Size" +msgstr "设置分割尺寸" + +msgid "Set details" +msgstr "集详情" + +msgid "Set the NFT chain priority within the SecBox table, lower values means higher priority." +msgstr "设置 SecBox 表格中的 NFT 链优先级,较低的值表示较高的优先级。" + +msgid "Set the NFT policy for SecBox-related Sets." +msgstr "设置 SecBox 相关集合的 NFT 策略。" + +msgid "Set the syslog level for NFT logging." +msgstr "设定 NFT 日志记录的 syslog 级别。" + +msgid "Settings" +msgstr "设置" + +msgid "Show Content" +msgstr "显示内容" + +msgid "Show only Set elements with hits" +msgstr "只显示有命中的集合元素" + +msgid "Split ASN Set" +msgstr "分割 ASN 集" + +msgid "Split Country Set" +msgstr "分割国家/地区集" + +msgid "Split external Set loading after every n members to save RAM, disabled by default." +msgstr "每 n 名成员后分割外部集合加载来节省内存,默认禁用。" + +msgid "Startup Trigger Interface" +msgstr "启动触发接口" + +msgid "Status" +msgstr "状态" + +msgid "Stop" +msgstr "停止" + +msgid "Subnet" +msgstr "子网" + +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "基于区域互联网注册机构(RIR)的地区概述。" + +msgid "System Info" +msgstr "系统信息" + +msgid "Table/Chain Settings" +msgstr "表/链设置" + +msgid "Target directory for SecBox-related error files." +msgstr "SecBox 相关错误文件的目标目录。" + +msgid "Target directory for SecBox-related report files." +msgstr "SecBox 相关报告文件的目标目录。" + +msgid "Target directory for compressed feed backups." +msgstr "压缩的源备份文件的目标目录。" + +msgid "The allowlist is too big, unable to save modifications." +msgstr "白名单太大,无法保存修改。" + +msgid "The blocklist is too big, unable to save modifications." +msgstr "黑名单太大,无法保存更改。" + +msgid "The selected ASNs are stored in separate Sets." +msgstr "所选的 ASN 被存储在不同的集合中。" + +msgid "The selected Countries are stored in separate Sets." +msgstr "所选国家/地区被存储在不同的集合中。" + +msgid "The selected priority will be used for SecBox background processing." +msgstr "所选的优先级将用于 SecBox 后台处理。" + +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "与 %s 相关的系统日志" + +msgid "This is the local SecBox allowlist that will permit certain MAC-, IP-addresses or domain names.
Please note: add only exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR notation and MAC/IP-bindings are allowed." +msgstr "这是本地 SecBox 白名单,会允许特定的 MAC、IP 地址或域名。
请注意: 每行只能添加一个 MAC/IPv4/IPv6 地址或域名。允许 CIDR 格式的 IP 范围和 MAC/IP 绑定。" + +msgid "This is the local SecBox blocklist that will prevent certain MAC-, IP-addresses or domain names.
Please note: add only exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR notation and MAC/IP-bindings are allowed." +msgstr "这是本地 SecBox 黑名单,会拦截特定的 MAC、IP 地址或域名。
请注意: 每行只能添加一个 MAC/IPv4/IPv6 地址或域名。允许 CIDR 格式的 IP 范围和 MAC/IP 绑定。" + +msgid "This report shows the latest NFT Set statistics, press the 'Refresh' button to get a new one. You can also display the specific content of Sets, search for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "此报告显示最新的 NFT 集合统计数字,请按下“刷新”按钮来获取新的报告。你也可以展示集合的特定内容、搜索可疑 IP;也可以在地图上展示这些 IP。" + +msgid "Timestamp" +msgstr "时间戳" + +msgid "To enable email notifications, set up the 'msmtp' package and specify a valid E-Mail receiver address." +msgstr "要启用电子邮件通知,设置 'msmtp' 包并指定有效的电子邮件接收者地址。" + +msgid "Token to communicate with the cgi interface." +msgstr "和 cgi 接口通信的令牌。" + +msgid "Topic for SecBox notification E-Mails." +msgstr "SecBox 通知邮件的主题。" + +msgid "Trigger Delay" +msgstr "触发延时" + +msgid "UDP-Threshold" +msgstr "UDP 阈值" + +msgid "UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "UDP 阈值用于防止 WAN DoS 攻击。要禁用此保护措施将其设置为 '0'。" + +msgid "URLv4" +msgstr "URLv4" + +msgid "URLv6" +msgstr "URLv6" + +msgid "Unable to parse the SecBox runtime information!" +msgstr "无法解析 SecBox 运行时信息!" + +msgid "Unable to parse the countries file!" +msgstr "无法解析国家文件!" + +msgid "Unable to parse the custom feed file!" +msgstr "无法解析自定义源文件!" + +msgid "Unable to parse the default feed file!" +msgstr "无法解析默认源文件!" + +msgid "Unable to parse the ruleset file!" +msgstr "无法解析规则集文件!" + +msgid "Unable to save modifications: %s" +msgstr "无法保存更改:%s" + +msgid "Upload" +msgstr "上传" + +msgid "Upload of the custom feed file failed." +msgstr "上传自定义源文件失败。" + +msgid "Verbose Debug Logging" +msgstr "详细的调试记录" + +msgid "With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). The file is located at '/etc/secbox/secbox.custom.feeds'. Then you can edit this file, delete entries, add new ones or make a local backup. To go back to the maintainers version just clear the custom feed file." +msgstr "使用此编辑器您可以上传本地自定义的源文件或填充一个初始文件(与软件包中提供的版本一模一样)。该文件位于 '/etc/secbox/secbox.custom.feeds'。然后,您可以编辑此文件删除条目添加新的条目或进行本地备份。要恢复到维护者版本只需清除自定义源文件即可。" + +msgid "alert" +msgstr "报警" + +msgid "asterisk invalid account" +msgstr "星号无效账号" + +msgid "auto-added IPs to allowlist" +msgstr "自动将 IP 添加到了白名单中" + +msgid "auto-added IPs to blocklist" +msgstr "自动将 IP 添加到了黑名单中" + +msgid "SecBox" +msgstr "网络安全" + +msgid "blocked bcp38 packets" +msgstr "拦截 bcp38 数据包" + +msgid "blocked icmp-flood packets" +msgstr "拦截了 icmp-flood 包" + +msgid "blocked invalid ct packets" +msgstr "拦截了无效的 ct 包" + +msgid "blocked invalid tcp packets" +msgstr "拦截了无效的 tcp 包" + +msgid "blocked syn-flood packets" +msgstr "拦截了 syn-flood 包" + +msgid "blocked udp-flood packets" +msgstr "拦截了 udp-flood 包" + +msgid "crit" +msgstr "关键项" + +msgid "debug" +msgstr "调试" + +msgid "drop" +msgstr "丢弃" + +msgid "dropbear failed login" +msgstr "dropbear 登录失败" + +msgid "emerg" +msgstr "紧急项" + +msgid "err" +msgstr "错误项" + +msgid "info" +msgstr "信息" + +msgid "local allowlist" +msgstr "本地白名单" + +msgid "local blocklist" +msgstr "本地黑名单" + +msgid "memory" +msgstr "内存" + +msgid "notice" +msgstr "通知" + +msgid "online documentation" +msgstr "在线文档" + +msgid "openvpn TLS error" +msgstr "openvpn TLS 错误" + +msgid "performance" +msgstr "性能" + +msgid "reject" +msgstr "拒绝" + +msgid "sshd closed connection" +msgstr "sshd 关闭了连接" + +msgid "sshd failed login" +msgstr "sshd 登录失败" + +msgid "warn" +msgstr "警告" + +msgid "The allowlist is too big, raw editor mode enforced." +msgstr "白名单过大,已强制使用纯文本编辑器模式。" + +msgid "System rule" +msgstr "系统规则" + +msgid "System Auto-Protected Rules" +msgstr "系统自动保护规则" + +msgid "rules" +msgstr "项规则" + +msgid "Auto detected by SecBox / protected" +msgstr "由 SecBox 自动检测 / 受保护" + +msgid "Delete this custom rule" +msgstr "删除此自定义规则" + +msgid "Delete" +msgstr "删除" + +msgid "Address / Domain / Network" +msgstr "地址 / 域名 / 网段" + +msgid "Comment" +msgstr "注释" + +msgid "Action" +msgstr "操作" + +msgid "No custom allowlist entries yet. Add one above." +msgstr "暂无自定义白名单条目,请在上方添加。" + +msgid "User Custom Rules" +msgstr "用户自定义规则" + +msgid "custom rules" +msgstr "项自定义规则" + +msgid "Switch to Raw Text Editor" +msgstr "切换到纯文本编辑器" + +msgid "e.g. 192.168.1.100, 10.0.0.0/24, or example.com" +msgstr "例如 192.168.1.100, 10.0.0.0/24 或 example.com" + +msgid "Optional comment (e.g. Office NAS)" +msgstr "可选注释(例如:办公室 NAS)" + +msgid "Please specify an IP, CIDR, Domain, or MAC address." +msgstr "请指定有效的 IP、CIDR、域名或 MAC 地址。" + +msgid "Invalid format: \"%s\" is not a valid IPv4, IPv6, CIDR, Domain or MAC." +msgstr "格式无效:\"%s\" 不是合法的 IPv4、IPv6、CIDR、域名或 MAC 地址。" + +msgid "Added custom rule \"%s\"." +msgstr "已添加自定义规则 \"%s\"。" + +msgid "Add User Custom Rule" +msgstr "添加用户自定义规则" + +msgid "Address or Domain" +msgstr "地址或域名" + +msgid "Comment / Note" +msgstr "注释 / 备注" + +msgid "+ Add Rule" +msgstr "+ 添加规则" + +msgid "Raw Text Editor" +msgstr "纯文本编辑器" + +msgid "Switch to Dual Table View" +msgstr "切换到双表格视图" + +msgid "One entry per line. Supports IPv4, IPv6, CIDR, MAC, Domain, and comments starting with \"#\"." +msgstr "每行一条记录。支持 IPv4、IPv6、CIDR、MAC、域名以及以 \"#\" 开头的注释。" + +msgid "SecBox Allowlist" +msgstr "网络安全白名单" + +msgid "Manage local trusted devices, subnets and domain names that will never be blocked by SecBox." +msgstr "管理受信任的本地设备、子网及域名,这些条目绝不会被 SecBox 拦截。" + +msgid "Reloading SecBox service..." +msgstr "正在重载 SecBox 服务..." + +msgid "Allowlist saved and SecBox service reloaded successfully!" +msgstr "白名单已保存,SecBox 服务重载成功!" + +msgid "Saved allowlist, but failed to reload SecBox: %s" +msgstr "已保存白名单,但重载 SecBox 失败:%s" + +msgid "Allowlist modifications have been saved." +msgstr "白名单修改已保存。" + +msgid "The blocklist is too big, raw editor mode enforced." +msgstr "黑名单过大,已强制使用纯文本编辑器模式。" + +msgid "Delete and unblock this rule" +msgstr "删除并解封此规则" + +msgid "Blocked Address / Domain / Network" +msgstr "拦截地址 / 域名 / 网段" + +msgid "Comment / Source" +msgstr "注释 / 来源" + +msgid "No blocked entries currently. Add one above." +msgstr "当前无黑名单拦截条目,请在上方添加。" + +msgid "Blocked Entries" +msgstr "黑名单拦截条目" + +msgid "active rules" +msgstr "项有效规则" + +msgid "e.g. 198.51.100.1, 203.0.113.0/24, or malicious-site.com" +msgstr "例如 198.51.100.1, 203.0.113.0/24 或 malicious-site.com" + +msgid "Optional comment (e.g. Malicious Bot)" +msgstr "可选注释(例如:恶意爬虫)" + +msgid "Added blocklist entry \"%s\"." +msgstr "已添加黑名单条目 \"%s\"。" + +msgid "Quick Add Blocklist Rule" +msgstr "快速添加黑名单规则" + +msgid "Switch to Table View" +msgstr "切换到表格视图" + +msgid "SecBox Blocklist" +msgstr "网络安全黑名单" + +msgid "Manage local blocked IP addresses, CIDR ranges, MAC addresses or domain names." +msgstr "管理本地拉黑的 IP 地址、CIDR 范围、MAC 地址或域名。" + +msgid "Blocklist saved and SecBox service reloaded successfully!" +msgstr "黑名单已保存,SecBox 服务重载成功!" + +msgid "Saved blocklist, but failed to reload SecBox: %s" +msgstr "已保存黑名单,但重载 SecBox 失败:%s" + +msgid "Blocklist modifications have been saved." +msgstr "黑名单修改已保存。" + +msgid "Executing action: %s..." +msgstr "正在执行操作:%s..." + +msgid "SecBox service action \"%s\" executed successfully." +msgstr "SecBox 服务操作 \"%s\" 执行成功。" + +msgid "Failed to execute action \"%s\": %s" +msgstr "执行操作 \"%s\" 失败:%s" + +msgid "Enabling and starting SecBox service..." +msgstr "正在启用并启动 SecBox 服务..." + +msgid "Stopping and disabling SecBox service..." +msgstr "正在停止并禁用 SecBox 服务..." + +msgid "SecBox service enabled and started." +msgstr "SecBox 服务已启用并成功启动。" + +msgid "SecBox service stopped and disabled." +msgstr "SecBox 服务已停止并成功禁用。" + +msgid "Failed to update service status: %s" +msgstr "更新服务状态失败:%s" + +msgid "Reload Service" +msgstr "重载服务" + +msgid "Restart Service" +msgstr "重启服务" + +msgid "Stop & Disable Service" +msgstr "停止并禁用服务" + +msgid "Start & Enable Service" +msgstr "启动并启用服务" + +msgid "SecBox Overview" +msgstr "网络安全概况" + +msgid "Real-time runtime status, active sets and blocking statistics for the SecBox service." +msgstr "SecBox 服务的实时运行状态、活动 IP 集及拦截统计。" + +msgid "Start Service" +msgstr "启动服务" + +msgid "Interfaces" +msgstr "接口信息" + +msgid "wan-dev" +msgstr "WAN 设备" + +msgid "wan-if" +msgstr "WAN 接口" + +msgid "vlan-allow" +msgstr "放行 VLAN" + +msgid "vlan-block" +msgstr "拦截 VLAN" + +msgid "uplink IPv4" +msgstr "上行 IPv4" + +msgid "uplink IPv6" +msgstr "上行 IPv6" + +msgid "Please enter or search an IP first." +msgstr "请先输入或搜索 IP 地址。" + +msgid "IP \"%s\" is already in %s." +msgstr "IP \"%s\" 已存在于 %s 中。" + +msgid "Successfully added \"%s\" to %s!" +msgstr "成功将 \"%s\" 添加到 %s!" + +msgid "Failed to update list: %s" +msgstr "更新列表失败:%s" + +msgid "IP Search & Quick Action" +msgstr "IP 搜索与快速操作" + +msgid "Search the SecBox-related Sets for a specific IP and take quick actions." +msgstr "在 SecBox 关联集合中检索指定 IP 并执行快捷操作。" + +msgid "Allowlist" +msgstr "白名单" + +msgid "+ Add to Allowlist" +msgstr "+ 加入白名单" + +msgid "Blocklist" +msgstr "黑名单" + +msgid "+ Add to Blocklist" +msgstr "+ 加入黑名单" + +msgid "Close" +msgstr "关闭" + +msgid "packet counters disabled" +msgstr "数据包计数器已禁用" + +msgid "SecBox Set Reporting" +msgstr "网络安全集合报表" + +msgid "Sets" +msgstr "IP 集合" + +msgid "Top Sets" +msgstr "命中最多集合" + +msgid "elements / packets" +msgstr "条目 / 数据包" + +msgid "no hits yet" +msgstr "暂无命中" + +msgid "Worst Sets" +msgstr "拦截最多集合" + +msgid "Blocked Packets" +msgstr "已拦截数据包" + +msgid "syn-flood" +msgstr "SYN 洪水" + +msgid "udp-flood" +msgstr "UDP 洪水" + +msgid "icmp-flood" +msgstr "ICMP 洪水" + +msgid "invalid ct" +msgstr "无效连接" + +msgid "invalid tcp" +msgstr "无效 TCP" + +msgid "bcp38" +msgstr "BCP38 过滤" + +msgid "Auto-added IPs" +msgstr "自动添加的 IP" + +msgid "allowlist" +msgstr "白名单" + +msgid "blocklist" +msgstr "黑名单" + +msgid "Applying configuration & reloading SecBox..." +msgstr "正在应用配置并重载 SecBox..." + +msgid "SecBox configuration saved and service %sed successfully." +msgstr "SecBox 配置已保存,服务已成功 %s。" + +msgid "Failed to %s SecBox: %s" +msgstr "%s SecBox 失败:%s" + +msgid "SecBox Settings" +msgstr "网络安全设置" + +msgid "Changes on this tab need a SecBox service restart to take effect." +msgstr "此标签页上的更改需要重启 SecBox 服务后生效。" + +msgid "Detect relevant network devices, interfaces, subnets, protocols and utilities automatically." +msgstr "自动检测相关的网络设备、接口、子网、协议和实用工具。" + +msgid "Override the pre-configured download options for the selected download utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the last parameter." +msgstr "覆盖所选下载工具的预配置下载参数。输出参数(如 curl 的 '-o' 或 wget 的 '-O')必须作为最后一个参数。" + +msgid "Number of download attempts in case of an error (not supported by uclient-fetch)." +msgstr "下载出错时的重试次数(uclient-fetch 不支持)。" + +msgid "Enable NFT counters for Set elements and chain rules. Required for the GeoIP Map and packet statistics in the Set Reporting." +msgstr "为集合条目和链规则启用 NFT 计数器。GeoIP 地图和集合报表统计需要此选项。" + +msgid "Enable a GeoIP Map with suspicious Set elements. This requires external requests to get the map tiles and geolocation data." +msgstr "启用可疑集合条目的 GeoIP 地图。这需要向外部发起请求以获取地图瓦片和地理位置数据。" + +msgid "Increase the maximal number of open files, e.g. to handle the amount of temporary split files while loading the Sets." +msgstr "增加最大打开文件数限制,例如在加载集合时处理大量临时分割文件。" + +msgid "Split external Set loading after every n members to save RAM, disabled by default." +msgstr "每隔 n 个成员分割外部集合加载以节省内存,默认禁用。" + +msgid "Deduplicate IP addresses across all active Sets and tidy up the local blocklist." +msgstr "在所有活动集合中对 IP 地址进行去重并整理本地黑名单。" + +msgid "Set the NFT chain priority within the SecBox table, lower values means higher priority." +msgstr "设置 SecBox 表内 NFT 链的优先级,数值越低优先级越高。" + +msgid "Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-Input and WAN-Forward chain." +msgstr "在 WAN-Input 和 WAN-Forward 链中始终放行带有特定端口或端口范围的协议 (tcp/udp)。" + +msgid "ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "每秒 ICMP 阈值包数,以防止 WAN-DoS 攻击。设为 '0' 可禁用此安全防护。" + +msgid "SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "每秒 SYN 阈值包数,以防止 WAN-DoS 攻击。设为 '0' 可禁用此安全防护。" + +msgid "UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to '0'." +msgstr "每秒 UDP 阈值包数,以防止 WAN-DoS 攻击。设为 '0' 可禁用此安全防护。" + +msgid "Override the default feed configuration and apply the feed to the inbound chain only." +msgstr "覆盖默认源配置,仅将该源应用于入站链(Inbound)。" + +msgid "Override the default feed configuration and apply the feed to the outbound chain only." +msgstr "覆盖默认源配置,仅将该源应用于出站链(Outbound)。" + +msgid "Override the default feed configuration and apply the feed to the inbound and outbound chain." +msgstr "覆盖默认源配置,将该源同时应用于入站与出站链。" + +msgid "Override the default feed configuration and remove existing port/protocol limitations." +msgstr "覆盖默认源配置并移除现有的端口/协议限制。" + +msgid "Location for parsing the log file, e.g. via syslog-ng, to deactivate the standard parsing via logread." +msgstr "解析日志文件的路径(例如通过 syslog-ng),以停用通过 logread 的标准解析。" + +msgid "Parse only the last stated number of log entries for suspicious events. To disable the log monitor at all set it to '0'." +msgstr "仅解析最后指定数量的日志条目以查找可疑事件。设为 '0' 可完全禁用日志监视器。" + +msgid "Number of failed login attempts of the same IP in the log before blocking." +msgstr "在日志中同一 IP 登录失败多少次后触发拉黑拦截。" + +msgid "Rate (per second) for the shared NFT log limit, applied globally across all logged rules. Set to '0' to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers." +msgstr "全局 NFT 日志记录速率限制(每秒条数)。设为 '0' 可完全禁用速率限制(例如使用 ulogd 时)。" + +msgid "Regular expressions to detect suspicious IPs in the system log. Uses the last IP per line; prefix a term with 'first:' for source-first formats, e.g. nginx." +msgstr "用于在系统日志中检测可疑 IP 的正则表达式。默认使用每行的最后一个 IP;前缀加 'first:' 可匹配源优先格式(如 nginx)。" + +msgid "To enable email notifications, set up the 'msmtp' package and specify a valid E-Mail receiver address." +msgstr "要启用邮件通知,请配置 'msmtp' 软件包并指定有效的收件人邮箱地址。" + +msgid "Receiver address for SecBox notification E-Mails, this information is required to enable E-Mail functionality." +msgstr "接收 SecBox 告警邮件的收件人地址,必须填写此项才能启用邮件功能。" + +msgid "Automatically add resolved domains and uplink IPs to the local SecBox allowlist." +msgstr "自动将解析出的域名及上行 IP 添加到本地 SecBox 白名单。" + +msgid "Automatically add resolved domains and suspicious IPs to the local SecBox blocklist." +msgstr "自动将解析出的域名及可疑 IP 添加到本地 SecBox 黑名单。" + +msgid "Automatically add entire subnets to the blocklist Set based on an additional RDAP request with the suspicious IP." +msgstr "基于可疑 IP 的 RDAP 查询结果,自动将整个所属子网添加到黑名单集合中。" + +msgid "Stop Service" +msgstr "停止服务" + +msgid "Table" +msgstr "表" + +msgid "Priority" +msgstr "优先级" + +msgid "Policy" +msgstr "策略" + +msgid "IPv4 Protocol" +msgstr "IPv4 协议" + +msgid "IPv6 Protocol" +msgstr "IPv6 协议" + +msgid "ICMP Limit" +msgstr "ICMP 限制" + +msgid "SYN Limit" +msgstr "SYN 限制" + +msgid "UDP Limit" +msgstr "UDP 限制" + +msgid "Log Service" +msgstr "日志服务" + +msgid "Download Tool" +msgstr "下载工具" + +msgid "WAN Device" +msgstr "WAN 设备" + +msgid "WAN Interface" +msgstr "WAN 接口" + +msgid "Allowed VLAN" +msgstr "放行 VLAN" + +msgid "Blocked VLAN" +msgstr "拦截 VLAN" + +msgid "Uplink IPv4" +msgstr "上行 IPv4" + +msgid "Uplink IPv6" +msgstr "上行 IPv6" + +msgid "Running (active)" +msgstr "运行中 (active)" + +msgid "Processing..." +msgstr "处理中..." + +msgid "Disabled" +msgstr "已禁用" + +msgid "Error" +msgstr "运行异常" + +msgid "Stopped" +msgstr "已停止" + +msgid "Not Running" +msgstr "未运行" + +msgid "Rule Elements" +msgstr "规则条目数" + +msgid "Configuration of the SecBox package to ban incoming and outgoing IPs via named nftables Sets. For further information please check the %s." +msgstr "SecBox 软件包配置,通过指定的 nftables 集合拦截入站和出站 IP。更多详细信息请参阅 %s。" + + +msgid "Live Log Console" +msgstr "实时日志控制台" + +msgid "Auto-scroll: ON" +msgstr "自动滚动: 开" + +msgid "Auto-scroll: OFF" +msgstr "自动滚动: 关" + +msgid "System Log Output" +msgstr "系统日志输出" + +msgid "Live Log Output" +msgstr "实时日志输出" + +msgid "No related logs" +msgstr "没有相关日志" + +msgid "Log Output" +msgstr "日志输出" + +msgid "Blocklist Entries" +msgstr "黑名单条目" + +msgid "Quick Add Block Rule" +msgstr "添加黑名单规则" + +msgid "Service Status" +msgstr "服务状态" + +msgid "NFT & Interface Information" +msgstr "NFT 与网络接口" + +msgid "Active Feeds & Flags" +msgstr "活动源与运行标志" + +msgid "Runtime Information" +msgstr "运行详细参数" + +msgid "Set Overview" +msgstr "集合概况" + +msgid "Blocked Packets & Auto-added IPs" +msgstr "攻击拦截与自动添加" + +msgid "Top Sets (Most Hits)" +msgstr "命中最多集合" + +msgid "Auto-added to Allowlist" +msgstr "自动添加至白名单" + +msgid "Auto-added to Blocklist" +msgstr "自动添加至黑名单" + +msgid "Hits" +msgstr "命中数" + +msgid "Top Sets (Most Hits)" +msgstr "命中最多集合" + +msgid "Worst Sets (Least Hits)" +msgstr "最少命中集合" + +msgid "Elements" +msgstr "规则条目" + +msgid "No custom allowlist entries yet. Add one below." +msgstr "暂无自定义白名单条目,请在下方添加。" + +msgid "Please specify a valid IP, CIDR, Domain or MAC address." +msgstr "请输入有效的 IP、CIDR 网段、域名或 MAC 地址。" + +msgid "Format invalid: \"%s\" is not a valid address without spaces." +msgstr "格式无效:“%s” 不是有效的无空格地址。" + +msgid "Add Rule" +msgstr "添加规则" + +msgid "One record per line. Supports IPv4, IPv6, CIDR, MAC, Domain and comments starting with \"#\"." +msgstr "每行一条记录。支持 IPv4、IPv6、CIDR 网段、MAC、域名以及以 “#” 开头的注释。" + +msgid "Manage trusted local devices, subnets and domains that will never be blocked by SecBox." +msgstr "管理受信任的本地设备、子网及域名,这些目标将永不被网络安全服务拦截。" + +msgid "Allowlist changes saved." +msgstr "白名单更改已成功保存。" + +msgid "Unable to save changes: %s" +msgstr "保存更改失败:%s" + +msgid "Currently no blocked entries. Add one below." +msgstr "暂无拦截条目,请在下方添加。" + +msgid "Manage locally blocked IP addresses, CIDR ranges, MAC addresses or domains." +msgstr "管理本地拦截的 IP 地址、CIDR 网段、MAC 地址或域名。" + +msgid "Blocklist changes saved." +msgstr "黑名单更改已成功保存。" + +msgid "Protocol/URL format not supported" +msgstr "不支持的协议/URL 格式" + +msgid "Version" +msgstr "版本" + +msgid "Rule Elements & Last Run" +msgstr "规则条目与最后运行" + +msgid "网络安全" +msgstr "网络安全" + +msgid "概况" +msgstr "概况" + +msgid "配置" +msgstr "配置" + +msgid "编辑白名单" +msgstr "编辑白名单" + +msgid "编辑黑名单" +msgstr "编辑黑名单" + +msgid "自定义源编辑器" +msgstr "自定义源编辑器" + +msgid "设置报告" +msgstr "设置报告" + +msgid "防火墙日志" +msgstr "防火墙日志" + +msgid "处理日志" +msgstr "处理日志" + +msgid "SecBox Configuration" +msgstr "网络安全配置" + +msgid "SecBox Custom Feed Editor" +msgstr "自定义源编辑器" + +msgid "SecBox Firewall Log" +msgstr "防火墙日志" + +msgid "SecBox Processing Log" +msgstr "处理日志" + +msgid "With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). The file is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, delete entries, add new ones or make a local backup. To go back to the maintainers version just clear the custom feed file." +msgstr "通过此编辑器,您可以上传本地自定义黑名单源文件或初始化一个初始文件(与软件包附带版本 1:1 完全一致的副本)。该文件位于 “/etc/banip/banip.custom.feeds”。随后您可以自由编辑该文件、删除条目、添加新源或创建本地备份。若要恢复为系统默认版本,只需清空自定义源文件即可。" + +msgid "Configuration" +msgstr "配置" + +msgid "Custom Feeds" +msgstr "自定义源编辑器" + +msgid "Running" +msgstr "运行中" diff --git a/luci-app-secbox/po/zh_Hant/secbox.po b/luci-app-secbox/po/zh_Hant/secbox.po new file mode 100644 index 000000000..9f6968f03 --- /dev/null +++ b/luci-app-secbox/po/zh_Hant/secbox.po @@ -0,0 +1,1405 @@ +msgid "" +msgstr "" +"PO-Revision-Date: 2026-08-01 07:02+0000\n" +"Last-Translator: 為什麼不加空格 \n" +"Language-Team: Chinese (Traditional Han script) \n" +"Language: zh_Hant\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Weblate 2026.8.dev0\n" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:641 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:710 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:738 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:750 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:786 +msgid "-- Please choose (optional) --" +msgstr "-- 請選擇 (可選) --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:99 +msgid "-- Set Selection --" +msgstr "-- 集合選擇 --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:280 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:311 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:322 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:333 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:344 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:382 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:416 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:430 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:444 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:461 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:471 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:480 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:513 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:521 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:529 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:537 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:545 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:568 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:594 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:613 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:626 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:814 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:838 +msgid "-- default --" +msgstr "-- 預設 --" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:239 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:240 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:241 +msgid "" +msgstr "<空間><網路遮罩>" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:242 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:243 +msgid "" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:745 +msgid "AFRINIC - serving Africa and the Indian Ocean region" +msgstr "AFRINIC - 為非洲和印度洋地區提供服務" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:746 +msgid "APNIC - serving the Asia Pacific region" +msgstr "APNIC - 為亞太區域提供服務" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:747 +msgid "ARIN - serving Canada and the United States" +msgstr "ARIN - 為加拿大和美國提供服務" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:759 +msgid "ASN Selection" +msgstr "ASN 選擇" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "ASNs" +msgstr "ASNs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:159 +msgid "Active Devices" +msgstr "活動裝置" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:155 +msgid "Active Feeds" +msgstr "活動訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:163 +msgid "Active Uplink" +msgstr "活動上行連結" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:639 +msgid "AdGuardHome login error" +msgstr "AdGuardHome 登入錯誤" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Additional trigger delay in seconds before banIP processing begins." +msgstr "BanIP 啟動前的額外觸發延遲 (秒)。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:195 +msgid "Advanced Settings" +msgstr "進階設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "Allow Protocol/Ports" +msgstr "允許的協定/連接埠" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Allow VLAN Forwards" +msgstr "允許 VLAN 轉發" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:774 +msgid "Allowlist Feed URLs" +msgstr "允許清單訂閱源 URLs" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Allowlist Only" +msgstr "僅允許清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:59 +msgid "" +"Allowlist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "允許清單的變更已儲存,重新載入 banIP 使其生效。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:387 +msgid "" +"Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-" +"Input and WAN-Forward chain." +msgstr "" +"在 WAN-Input 與 WAN-Forward 鏈中始終允許特定 (tcp/udp) 連接埠或連接埠範圍。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:391 +msgid "Always allow certain VLAN forwards." +msgstr "始終允許特定 VLAN 轉發。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Always block certain VLAN forwards." +msgstr "始終封鎖特定 VLAN 轉發。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Auto Allow Uplink" +msgstr "自動允許上行連結" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "Auto Allowlist" +msgstr "自動允許清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "Auto Block Subnet" +msgstr "自動封鎖子網路" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "Auto Blocklist" +msgstr "自動封鎖清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "Auto Detection" +msgstr "自動偵測" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:823 +msgid "" +"Automatically add entire subnets to the blocklist Set based on an additional " +"RDAP request with the suspicious IP." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:819 +msgid "" +"Automatically add resolved domains and suspicious IPs to the local banIP " +"blocklist." +msgstr "自動將已解析的網域與可疑 IP 加入本機 banIP 封鎖清單。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:804 +msgid "" +"Automatically add resolved domains and uplink IPs to the local banIP " +"allowlist." +msgstr "自動將已解析的網域與上行連結 IP 加入本機 banIP 允許清單。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Backup Directory" +msgstr "備份目錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base Directory" +msgstr "基礎目錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:348 +msgid "Base working directory while banIP processing." +msgstr "banIP 處理時的基礎運作目錄。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:397 +msgid "Block VLAN Forwards" +msgstr "封鎖 VLAN 轉發" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Block packets with spoofed source IP addresses in all supported chains." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:703 +msgid "Blocklist Feed" +msgstr "封鎖清單訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Blocklist Set Expiry" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:59 +msgid "" +"Blocklist modifications have been saved, reload banIP that changes take " +"effect." +msgstr "封鎖清單的變更已儲存,重新載入 banIP 使其生效。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Burst size in packets for the shared NFT log limit." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "CPU Cores" +msgstr "CPU 核心數" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:40 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:144 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:193 +msgid "Cancel" +msgstr "取消" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:247 +msgid "Chain" +msgstr "鏈" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "Chain Priority" +msgstr "鏈優先級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:698 +msgid "Changes on this tab needs a banIP service reload to take effect." +msgstr "此分頁上的變更需要重新載入 banIP 服務使其生效。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:207 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:301 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:373 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:454 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:555 +msgid "Changes on this tab needs a banIP service restart to take effect." +msgstr "此分頁上的變更需要重新啟動 banIP 服務使其生效。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:312 +msgid "Clear" +msgstr "清除" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:761 +msgid "Collection of IP addresses based on Autonomous System Numbers." +msgstr "基於 ASN 的 IP 位址集。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:47 +msgid "" +"Configuration of the banIP package to ban incoming and outgoing IPs via " +"named nftables Sets. For further information please check the %s." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:234 +msgid "Count" +msgstr "計數" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:723 +msgid "Countries" +msgstr "國家" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:717 +msgid "Country Selection" +msgstr "國家選擇" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:49 +msgid "Custom Feed Editor" +msgstr "自訂訂閱源編輯器" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "" +"Deduplicate IP addresses across all active Sets and tidy up the local " +"blocklist." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:364 +msgid "Deduplicate IPs" +msgstr "刪除重複 IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:253 +msgid "Description" +msgstr "描述" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:216 +msgid "" +"Detect relevant network devices, interfaces, subnets, protocols and " +"utilities automatically." +msgstr "自動偵測相關的網路裝置、介面、子網路、協定和工具。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:810 +msgid "Disable" +msgstr "停用" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Don't check SSL server certificates during download." +msgstr "下載時不檢查伺服器的 SSL 憑證。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:285 +msgid "Download" +msgstr "下載" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:285 +msgid "Download Insecure" +msgstr "不安全下載" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "Download Parameters" +msgstr "下載參數" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "Download Retries" +msgstr "下載重試次數" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Download Utility" +msgstr "下載工具" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Drop packets silently or actively reject Inbound traffic." +msgstr "靜靜地丟棄封包或積極地拒絕傳入流量。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "E-Mail Notification" +msgstr "電子郵件通知" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "E-Mail Profile" +msgstr "電子郵件設定檔" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "E-Mail Receiver Address" +msgstr "電子郵件收件者位址" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "E-Mail Sender Address" +msgstr "電子郵件發送者位址" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:199 +msgid "E-Mail Settings" +msgstr "電子郵件設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "E-Mail Topic" +msgstr "電子郵件主旨" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:33 +msgid "Edit Allowlist" +msgstr "編輯允許清單" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:41 +msgid "Edit Blocklist" +msgstr "編輯封鎖清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:151 +msgid "Element Count" +msgstr "元素數量" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:238 +msgid "Elements (max. 50)" +msgstr "元素 (最多 50 個)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:208 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:257 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:655 +msgid "Empty field not allowed" +msgstr "不允許留空" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:403 +msgid "Enable BCP38" +msgstr "啟用 BCP38" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "Enable GeoIP Map" +msgstr "啟用 GeoIP 地圖" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "" +"Enable NFT counters for Set elements and chain rules. Required for the GeoIP " +"Map and packet statistics in the Set Reporting." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable Remote Logging" +msgstr "啟用遠端紀錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:291 +msgid "" +"Enable a GeoIP Map with suspicious Set elements. This requires external " +"requests to get the map tiles and geolocation data." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enable the banIP service." +msgstr "啟用 banIP 服務。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:645 +msgid "Enable the cgi interface to receive remote logging events." +msgstr "啟用遠端 cgi 介面以接收遠端紀錄事件。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Enable verbose debug logging in case of processing errors." +msgstr "若遇處理錯誤啟用詳細除錯記錄。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:210 +msgid "Enabled" +msgstr "已啟用" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "Enables IPv4 support." +msgstr "啟用 IPv4 支援。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "Enables IPv6 support." +msgstr "啟用 IPv6 支援。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Error Directory" +msgstr "錯誤目錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:828 +msgid "Expiry time for auto added blocklist Set members." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:771 +msgid "External Allowlist Feeds" +msgstr "外部允許清單訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:700 +msgid "External Blocklist Feeds" +msgstr "外部封鎖清單訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:380 +msgid "Failed to generate a banIP report!" +msgstr "無法產生 banIP 報告!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Feed Complete" +msgstr "訂閱源完成" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "Feed Flag Reset" +msgstr "訂閱源旗標重設" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:203 +msgid "Feed Name" +msgstr "訂閱源名稱" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:200 +msgid "Feed Selection" +msgstr "訂閱源選擇" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:197 +msgid "Feed/Set Settings" +msgstr "訂閱源/集合設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:303 +msgid "Fill" +msgstr "填充" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:65 +msgid "Firewall Log" +msgstr "防火牆紀錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:262 +msgid "Flag" +msgstr "旗標" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:268 +msgid "Flag not supported" +msgstr "旗標不受支援" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:194 +msgid "General Settings" +msgstr "一般設定" + +#: applications/luci-app-banip/root/usr/share/rpcd/acl.d/luci-app-banip.json:3 +msgid "Grant access to LuCI app banIP" +msgstr "授予存取 luci-app-banip 的權限" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:306 +msgid "High Priority" +msgstr "較高優先級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:305 +msgid "Highest Priority" +msgstr "最高優先級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "ICMP-Threshold" +msgstr "ICMP 閾值" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:407 +msgid "" +"ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"每秒 ICMP 封包數的閾值,用以防止 WAN 遭受 DoS 攻擊。要停用此安全措施,請將其" +"設定為 '0'。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:812 +msgid "IP" +msgstr "IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:12 +msgid "IP Search" +msgstr "IP 搜尋" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:350 +msgid "IP Search..." +msgstr "IP 搜尋..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "IPv4 Network Interfaces" +msgstr "IPv4 網路介面" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:219 +msgid "IPv4 Support" +msgstr "IPv4 支援" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "IPv6 Network Interfaces" +msgstr "IPv6 網路介面" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:224 +msgid "IPv6 Support" +msgstr "IPv6 支援" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:248 +msgid "Inbound" +msgstr "傳入" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:250 +msgid "Inbound & Outbound" +msgstr "傳入與傳出" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "Inbound & Outbound Feed" +msgstr "傳入和傳出訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:476 +msgid "Inbound Block Policy" +msgstr "傳入封鎖策略" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "Inbound Feed" +msgstr "傳入訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:235 +msgid "Inbound (packets)" +msgstr "傳入 (封包)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "" +"Increase the maximal number of open files, e.g. to handle the amount of " +"temporary split files while loading the Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:145 +msgid "Information" +msgstr "資訊" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:794 +msgid "Invalid URL format" +msgstr "無效 URL 格式" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:211 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:658 +msgid "Invalid characters" +msgstr "無效字元" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:846 +msgid "Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:125 +msgid "Invalid input values, unable to save modifications." +msgstr "輸入值無效,無法儲存變更。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:748 +msgid "LACNIC - serving the Latin American and Caribbean region" +msgstr "LACNIC - 為拉丁美洲和加勒比海地區提供服務" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:179 +msgid "Last Run" +msgstr "最後執行" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:309 +msgid "Least Priority" +msgstr "最低優先級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:308 +msgid "Less Priority" +msgstr "較低優先級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:327 +msgid "Limit the cpu cores used by banIP to save RAM, autodetected by default." +msgstr "限制 banIP 使用的 cpu 核心數以節省記憶體,預設為自動偵測。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:808 +msgid "Limit the uplink autoallow function." +msgstr "限制上行連結自動允許功能。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "List of available network interfaces to trigger the banIP start." +msgstr "觸發 banIP 啟動的可用網路介面清單。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:107 +msgid "List the elements of a specific banIP-related Set." +msgstr "列出與某一特定 banIP 有關的集合的元素。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:802 +msgid "Local Feed Settings" +msgstr "本地訂閱源設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "" +"Location for parsing the log file, e.g. via syslog-ng, to deactivate the " +"standard parsing via logread." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:618 +msgid "Log Burst Limit" +msgstr "記錄突發限制" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "Log Count" +msgstr "紀錄計數" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log Inbound" +msgstr "記錄傳入" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "Log Limit" +msgstr "紀錄限制" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log Outbound" +msgstr "記錄傳出" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log Prerouting" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "Log Rate Limit" +msgstr "記錄速率限制" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:198 +msgid "Log Settings" +msgstr "紀錄設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:631 +msgid "Log Terms" +msgstr "紀錄項目" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:579 +msgid "Log suspicious packets in the LAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:573 +msgid "Log suspicious packets in the Prerouting chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:576 +msgid "Log suspicious packets in the WAN-Input and WAN-Forward chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:582 +msgid "Logfile Location" +msgstr "紀錄檔案位置" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:634 +msgid "LuCI failed login" +msgstr "LuCI 登入失敗" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:201 +msgid "Map Reset" +msgstr "地圖重設" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:336 +msgid "Map..." +msgstr "地圖..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:316 +msgid "Max Open Files" +msgstr "開啟檔案的最大數目" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:167 +msgid "NFT Information" +msgstr "NFT 資訊" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "NFT Log Level" +msgstr "NFT 紀錄等級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Network Devices" +msgstr "網路裝置" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:156 +msgid "Network error" +msgstr "網路錯誤" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "Nice Level" +msgstr "Nice 等級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:31 +msgid "No %s related logs yet!" +msgstr "還沒有 %s 相關紀錄!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:332 +msgid "No GeoIP Map data!" +msgstr "沒有 GeoIP 地圖資料!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:42 +msgid "No banIP config found!" +msgstr "找不到 banIP 配置!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:307 +msgid "Normal Priority" +msgstr "正常優先級" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Number of Set load attempts in case of an error." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:273 +msgid "" +"Number of download attempts in case of an error (not supported by uclient-" +"fetch)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:599 +msgid "" +"Number of failed login attempts of the same IP in the log before blocking." +msgstr "進行封鎖前允許同一 IP 的失敗登入嘗試在紀錄中出現幾次。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:541 +msgid "Opt out specific feeds from the deduplication process." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:249 +msgid "Outbound" +msgstr "傳出" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "Outbound Feed" +msgstr "傳出訂閱源" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:236 +msgid "Outbound (packets)" +msgstr "傳出 (封包)" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:525 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"and outbound chain." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:509 +msgid "" +"Override the default feed configuration and apply the feed to the inbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:517 +msgid "" +"Override the default feed configuration and apply the feed to the outbound " +"chain only." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:533 +msgid "" +"Override the default feed configuration and remove existing port/protocol " +"limitations." +msgstr "覆寫預設訂閱源配置並移除現存連接埠/協定限制。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:258 +msgid "" +"Override the pre-configured download options for the selected download " +"utility. The output flag, e.g. '-o' for curl or '-O' for wget, must be the " +"last parameter." +msgstr "" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:25 +msgid "Overview" +msgstr "概覽" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:586 +msgid "" +"Parse only the last stated number of log entries for suspicious events. To " +"disable the log monitor at all set it to '0'." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:237 +msgid "Port / Protocol" +msgstr "連接埠 / 協定" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:73 +msgid "Processing Log" +msgstr "處理紀錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:688 +msgid "Profile used by 'msmtp' for banIP notification E-Mails." +msgstr "「msmtp」所用的 banIP 電子郵件通知設定。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:749 +msgid "RIPE - serving Europe, Middle East and Central Asia" +msgstr "RIPE - 為歐洲、中東和中亞提供服務" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:604 +msgid "" +"Rate (per second) for the shared NFT log limit, applied globally across all " +"logged rules. Set to '0' to disable rate limiting entirely, e.g. when using " +"ulogd or other userspace log handlers." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:673 +msgid "Receive E-Mail notifications with every banIP run." +msgstr "每次 banIP 執行都接收電子郵件通知。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:676 +msgid "" +"Receiver address for banIP notification E-Mails, this information is " +"required to enable E-Mail functionality." +msgstr "banIP 通知電子郵件的接收位址,要開啟電子郵件功能必須填寫此資訊。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:387 +msgid "Refresh" +msgstr "重新整理" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Regional Internet Registry" +msgstr "區域網際網路註冊管理機構" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:632 +msgid "" +"Regular expressions to detect suspicious IPs in the system log. Uses the " +"last IP per line; prefix a term with 'first:' for source-first formats, e.g. " +"nginx." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Remote Token" +msgstr "遠端權杖" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:640 +msgid "Remote logging Event" +msgstr "遠端記錄事件" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Report Directory" +msgstr "報告目錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:288 +msgid "Reporting Counters" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:851 +msgid "Restrict the internet access from/to a small number of secure IPs." +msgstr "限制來自/對少量安全 IP 的網際網路存取。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:26 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:130 +msgid "Result" +msgstr "結果" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:238 +msgid "Rule" +msgstr "規則" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:175 +msgid "Run Flags" +msgstr "執行旗標" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:171 +msgid "Run Information" +msgstr "執行資訊" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "SYN-Threshold" +msgstr "SYN 閾值" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:421 +msgid "" +"SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"每秒 SYN 封包數的閾值,用以防止 WAN 遭受 DoS 攻擊。要停用此安全措施,請將其設" +"定為 '0'。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:321 +msgid "Save" +msgstr "儲存" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:870 +msgid "Save & Reload" +msgstr "儲存並重新載入" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:877 +msgid "Save & Restart" +msgstr "儲存並重新啟動" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:77 +msgid "Search IP" +msgstr "搜尋 IP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:49 +msgid "Search is running, please wait..." +msgstr "搜尋執行中,請稍後..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:13 +msgid "Search the banIP-related Sets for a specific IP." +msgstr "在 banIP 相關的集合中搜尋一個特定的 IP。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:68 +msgid "Search timed out." +msgstr "搜尋逾時。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:250 +msgid "Select one of the pre-configured download utilities." +msgstr "選擇一個預配置的下載工具。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:229 +msgid "Select the WAN network device(s)." +msgstr "選擇 WAN 網路裝置。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:236 +msgid "Select the logical WAN IPv4 network interface(s)." +msgstr "選擇邏輯 WAN IPv4 網路介面。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:243 +msgid "Select the logical WAN IPv6 network interface(s)." +msgstr "選擇邏輯 WAN IPv6 網路介面。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:680 +msgid "Sender address for banIP notification E-Mails." +msgstr "banIP 通知郵件的傳送位址。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:110 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:233 +msgid "Set" +msgstr "設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:106 +msgid "Set Content" +msgstr "設定內容" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:343 +msgid "Set Content..." +msgstr "設定內容..." + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:466 +msgid "Set Load Retries" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set Policy" +msgstr "設定規則" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:57 +msgid "Set Reporting" +msgstr "設定報告" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "Set Split Size" +msgstr "設定分割尺寸" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:315 +msgid "Set details" +msgstr "集合詳情" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:376 +msgid "" +"Set the NFT chain priority within the banIP table, lower values means higher " +"priority." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:457 +msgid "Set the NFT policy for banIP-related Sets." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:558 +msgid "Set the syslog level for NFT logging." +msgstr "設定 NFT 紀錄記錄的 syslog 等級。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:192 +msgid "Settings" +msgstr "設定" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:162 +msgid "Show Content" +msgstr "顯示內容" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:126 +msgid "Show only Set elements with hits" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "Split ASN Set" +msgstr "分開 ASN 集合" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "Split Country Set" +msgstr "分開國家集合" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:337 +msgid "" +"Split external Set loading after every n members to save RAM, disabled by " +"default." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:263 +msgid "Startup Trigger Interface" +msgstr "啟動觸發介面" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:147 +msgid "Status" +msgstr "狀態" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:863 +msgid "Stop" +msgstr "停止" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:811 +msgid "Subnet" +msgstr "子網路" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:744 +msgid "Summary of countries based on the Regional Internet Registry (RIR)." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:183 +msgid "System Info" +msgstr "系統資訊" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:196 +msgid "Table/Chain Settings" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:360 +msgid "Target directory for banIP-related error files." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:356 +msgid "Target directory for banIP-related report files." +msgstr "banIP 相關報告檔案的目標目錄。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:352 +msgid "Target directory for compressed feed backups." +msgstr "壓縮的訂閱源備份目標目錄。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:35 +msgid "The allowlist is too big, unable to save modifications." +msgstr "允許清單過大,無法儲存變更。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:35 +msgid "The blocklist is too big, unable to save modifications." +msgstr "封鎖清單過大,無法儲存變更。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:766 +msgid "The selected ASNs are stored in separate Sets." +msgstr "所選的 ASN 被儲存在不同的集合中。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:754 +msgid "The selected Countries are stored in separate Sets." +msgstr "所有國家被儲存在不同的集合中。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:304 +msgid "The selected priority will be used for banIP background processing." +msgstr "所選的優先順序將用於 banIP 背景處理。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/logtemplate.js:42 +msgid "The syslog output, pre-filtered for messages related to: %s" +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:38 +msgid "" +"This is the local banIP allowlist that will permit certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:38 +msgid "" +"This is the local banIP blocklist that will prevent certain MAC-, IP-" +"addresses or domain names.
Please note: add only " +"exactly one MAC/IPv4/IPv6 address or domain name per line. Ranges in CIDR " +"notation and MAC/IP-bindings are allowed." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:271 +msgid "" +"This report shows the latest NFT Set statistics, press the 'Refresh' button " +"to get a new one. You can also display the specific content of Sets, search " +"for suspicious IPs and finally, these IPs can also be displayed on a map." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:274 +msgid "Timestamp" +msgstr "時間戳" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:670 +msgid "" +"To enable email notifications, set up the 'msmtp' package and specify a " +"valid E-Mail receiver address." +msgstr "要啟用電子郵件通知,請安裝 'msmtp' 套件並指定有效的電子郵件收件者位址。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:650 +msgid "Token to communicate with the cgi interface." +msgstr "與 cgi 介面通訊使用的權杖。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:684 +msgid "Topic for banIP notification E-Mails." +msgstr "banIP 通知郵件的主題。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:268 +msgid "Trigger Delay" +msgstr "觸發延遲" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "UDP-Threshold" +msgstr "UDP 閾值" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:435 +msgid "" +"UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable " +"this safeguard set it to '0'." +msgstr "" +"每秒 UDP 封包數的閾值,用以防止 WAN 遭受 DoS 攻擊。要停用此安全措施,請將其設" +"定為 '0'。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:216 +msgid "URLv4" +msgstr "URLv4" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:227 +msgid "URLv6" +msgstr "URLv6" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:91 +msgid "Unable to parse the banIP runtime information!" +msgstr "無法解析 banIP 執行時期資訊!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:733 +msgid "Unable to parse the countries file!" +msgstr "無法解析國家檔案!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:494 +msgid "Unable to parse the custom feed file!" +msgstr "無法解析自訂訂閱源檔案!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:501 +msgid "Unable to parse the default feed file!" +msgstr "無法解析預設訂閱源檔案!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:93 +msgid "Unable to parse the ruleset file!" +msgstr "無法解析規則集檔案!" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/allowlist.js:63 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/blocklist.js:63 +msgid "Unable to save modifications: %s" +msgstr "無法儲存變更: %s" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:294 +msgid "Upload" +msgstr "上傳" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:85 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:91 +msgid "Upload of the custom feed file failed." +msgstr "上傳自訂訂閱源檔案失敗。" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:213 +msgid "Verbose Debug Logging" +msgstr "詳細除錯紀錄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/feeds.js:187 +msgid "" +"With this editor you can upload your local custom feed file or fill up an " +"initial one (a 1:1 copy of the version shipped with the package). The file " +"is located at '/etc/banip/banip.custom.feeds'. Then you can edit this file, " +"delete entries, add new ones or make a local backup. To go back to the " +"maintainers version just clear the custom feed file." +msgstr "" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:560 +msgid "alert" +msgstr "警示" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:637 +msgid "asterisk invalid account" +msgstr "星號無效帳號" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:304 +msgid "auto-added IPs to allowlist" +msgstr "自動新增 IP 到允許清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:308 +msgid "auto-added IPs to blocklist" +msgstr "自動新增 IP 到封鎖清單" + +#: applications/luci-app-banip/root/usr/share/luci/menu.d/luci-app-banip.json:3 +msgid "banIP" +msgstr "banIP" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:299 +msgid "blocked bcp38 packets" +msgstr "封鎖 bcp38 封包" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:287 +msgid "blocked icmp-flood packets" +msgstr "封鎖 icmp 洪水封包" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:291 +msgid "blocked invalid ct packets" +msgstr "封鎖無效 ct 封包" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:295 +msgid "blocked invalid tcp packets" +msgstr "封鎖無效 tcp 封包" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:279 +msgid "blocked syn-flood packets" +msgstr "封鎖 syn 洪水封包" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/setreport.js:283 +msgid "blocked udp-flood packets" +msgstr "封鎖 udp 洪水封包" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:561 +msgid "crit" +msgstr "關鍵" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:566 +msgid "debug" +msgstr "除錯" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:477 +msgid "drop" +msgstr "丟棄" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:633 +msgid "dropbear failed login" +msgstr "dropbear 登入失敗" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:559 +msgid "emerg" +msgstr "緊急" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:562 +msgid "err" +msgstr "錯誤" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:565 +msgid "info" +msgstr "資訊" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:510 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:518 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:526 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:534 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:542 +msgid "local allowlist" +msgstr "本地允許清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:511 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:519 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:527 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:535 +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:543 +msgid "local blocklist" +msgstr "本地封鎖清單" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:458 +msgid "memory" +msgstr "記憶體" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:564 +msgid "notice" +msgstr "通知" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:48 +msgid "online documentation" +msgstr "線上文件" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:638 +msgid "openvpn TLS error" +msgstr "openvpn TLS 錯誤" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:459 +msgid "performance" +msgstr "效能" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:478 +msgid "reject" +msgstr "拒絕" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:636 +msgid "sshd closed connection" +msgstr "sshd 已關閉連線" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:635 +msgid "sshd failed login" +msgstr "sshd 登入失敗" + +#: applications/luci-app-banip/htdocs/luci-static/resources/view/banip/overview.js:563 +msgid "warn" +msgstr "警告" + +#~ msgid "Regular expressions to detect suspicious IPs in the system log." +#~ msgstr "用來在系統紀錄中偵測可疑 IP 的正規表達式。" + +#~ msgid "" +#~ "Override the pre-configured download options for the selected download " +#~ "utility." +#~ msgstr "覆寫所選下載工具預配置的下載選項。" + +#~ msgid "Set Element Counter" +#~ msgstr "設定元素計數器" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "nginx suspicious IP" +#~ msgstr "nginx 可疑 IP" + +#~ msgid " " +#~ msgstr " <網路遮罩>" + +#~ msgid "Protocol/URL format not supported" +#~ msgstr "協定 / URL 格式不受支援" + +#~ msgid "Unable to parse the runtime information!" +#~ msgstr "無法解析執行時期資訊!" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid ", substring" +#~ msgstr ", substring" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", nodups" +#~ msgstr ", nodups" + +#~ msgid "" +#~ msgstr "" + +#~ msgid ", concatinated" +#~ msgstr ", concatinated" + +#~ msgid "NET" +#~ msgstr "NET" + +#~ msgid "YOUR" +#~ msgstr "YOUR" + +#~ msgid "[|]" +#~ msgstr "[|]" + +#~ msgid ", csv" +#~ msgstr ", csv" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "" +#~ msgstr "" + +#~ msgid "Version" +#~ msgstr "版本" + +#~ msgid "Elements" +#~ msgstr "元素" + +#~ msgid "GeoIP Map is not enabled!" +#~ msgstr "GeoIP地圖沒有啟用!" diff --git a/luci-app-secbox/root/etc/uci-defaults/95-luci-app-secbox-housekeeping b/luci-app-secbox/root/etc/uci-defaults/95-luci-app-secbox-housekeeping new file mode 100644 index 000000000..e6e19f9f2 --- /dev/null +++ b/luci-app-secbox/root/etc/uci-defaults/95-luci-app-secbox-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-secbox/root/usr/share/luci/menu.d/luci-app-secbox.json b/luci-app-secbox/root/usr/share/luci/menu.d/luci-app-secbox.json new file mode 100644 index 000000000..ba2da2463 --- /dev/null +++ b/luci-app-secbox/root/usr/share/luci/menu.d/luci-app-secbox.json @@ -0,0 +1,88 @@ +{ + "admin/services/secbox": { + "title": "SecBox", + "order": "60", + "action": { + "type": "alias", + "path": "admin/services/secbox/overview" + }, + "depends": { + "acl": [ + "luci-app-secbox" + ], + "fs": { + "/usr/bin/banip-service.sh": "executable", + "/etc/init.d/banip": "executable", + "/etc/banip/banip.feeds": "file", + "/etc/banip/banip.countries": "file" + }, + "uci": { + "banip": true + } + } + }, + "admin/services/secbox/overview": { + "title": "Overview", + "order": 10, + "action": { + "type": "view", + "path": "secbox/overview" + } + }, + "admin/services/secbox/settings": { + "title": "Configuration", + "order": 20, + "action": { + "type": "view", + "path": "secbox/settings" + } + }, + "admin/services/secbox/allowlist": { + "title": "Allowlist", + "order": 30, + "action": { + "type": "view", + "path": "secbox/allowlist" + } + }, + "admin/services/secbox/blocklist": { + "title": "Blocklist", + "order": 40, + "action": { + "type": "view", + "path": "secbox/blocklist" + } + }, + "admin/services/secbox/feeds": { + "title": "Custom Feeds", + "order": 50, + "action": { + "type": "view", + "path": "secbox/feeds" + } + }, + "admin/services/secbox/setreport": { + "title": "Set Reporting", + "order": 60, + "action": { + "type": "view", + "path": "secbox/setreport" + } + }, + "admin/services/secbox/firewall_log": { + "title": "Firewall Log", + "order": 70, + "action": { + "type": "view", + "path": "secbox/firewall_log" + } + }, + "admin/services/secbox/processing_log": { + "title": "Processing Log", + "order": 80, + "action": { + "type": "view", + "path": "secbox/processing_log" + } + } +} diff --git a/luci-app-secbox/root/usr/share/rpcd/acl.d/luci-app-secbox.json b/luci-app-secbox/root/usr/share/rpcd/acl.d/luci-app-secbox.json new file mode 100644 index 000000000..1dab362f7 --- /dev/null +++ b/luci-app-secbox/root/usr/share/rpcd/acl.d/luci-app-secbox.json @@ -0,0 +1,112 @@ +{ + "luci-app-secbox": { + "description": "Grant access to LuCI app SecBox", + "write": { + "cgi-io": [ + "upload" + ], + "ubus": { + "file": [ + "write" + ] + }, + "uci": [ + "banip" + ], + "file": { + "/etc/banip/banip.allowlist": [ + "write" + ], + "/etc/banip/banip.blocklist": [ + "write" + ], + "/etc/banip/banip.custom.feeds": [ + "write" + ], + "/var/run/banIP/banIP.search": [ + "write" + ], + "/tmp/run/banIP/banIP.search": [ + "write" + ], + "/var/run/banIP/banIP.report": [ + "write" + ], + "/tmp/run/banIP/banIP.report": [ + "write" + ] + } + }, + "read": { + "cgi-io": [ + "download", + "exec" + ], + "file": { + "/etc/banip/*": [ + "read", + "list" + ], + "/var/run/banIP/banIP.runtime.json": [ + "read", + "list" + ], + "/tmp/run/banIP/banIP.runtime.json": [ + "read", + "list" + ], + "/var/run/banIP/banIP.search": [ + "read" + ], + "/tmp/run/banIP/banIP.search": [ + "read" + ], + "/var/run/banIP/banIP.report": [ + "read" + ], + "/tmp/run/banIP/banIP.report": [ + "read" + ], + "/usr/sbin/nft -tj list sets": [ + "exec" + ], + "/etc/init.d/banip stop": [ + "exec" + ], + "/etc/init.d/banip reload": [ + "exec" + ], + "/etc/init.d/banip restart": [ + "exec" + ], + "/etc/init.d/banip actual": [ + "exec" + ], + "/etc/init.d/banip report json": [ + "exec" + ], + "/etc/init.d/banip report gen": [ + "exec" + ], + "/etc/init.d/banip search *": [ + "exec" + ], + "/etc/init.d/banip content *": [ + "exec" + ] + }, + "ubus": { + "file": [ + "read", + "stat" + ] + }, + "uci": [ + "banip" + ], + "log": [ + "read" + ] + } + } +} diff --git a/luci-app-shadcnui-config/Makefile b/luci-app-shadcnui-config/Makefile new file mode 100644 index 000000000..f45257006 --- /dev/null +++ b/luci-app-shadcnui-config/Makefile @@ -0,0 +1,21 @@ +# +# Copyright (C) 2026 bsatou +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=shadcnui Theme Configuration +LUCI_DEPENDS:=+luci-base +luci-theme-shadcnui +curl +jsonfilter +LUCI_PKGARCH:=all + +PKG_NAME:=luci-app-shadcnui-config +PKG_VERSION:=0.3.3 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 +PKG_MAINTAINER:=bsatou + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-shadcnui-config/htdocs/luci-static/resources/view/shadcnui/config.js b/luci-app-shadcnui-config/htdocs/luci-static/resources/view/shadcnui/config.js new file mode 100644 index 000000000..7ba242bac --- /dev/null +++ b/luci-app-shadcnui-config/htdocs/luci-static/resources/view/shadcnui/config.js @@ -0,0 +1,482 @@ +'use strict'; +'require view'; +'require ui'; +'require rpc'; +'require fs'; + +/* + * shadcnui theme configuration page. + * Lives at /usr/share/luci/menu.d/luci-app-shadcnui-config.json -> view shadcnui/config + * Talks to /usr/libexec/rpcd/luci.shadcnui via ubus. + */ + +var rpcList = rpc.declare({ + object: 'luci.shadcnui', method: 'list', + expect: { config: {} } +}); +var rpcSave = rpc.declare({ + object: 'luci.shadcnui', method: 'save', + params: [ 'mode', 'base', 'accent', 'radius', 'bg_source', 'bg_url', + 'unsplash_key', 'wallhaven_key' ], + expect: { ok: false } +}); +var rpcBing = rpc.declare({ + object: 'luci.shadcnui', method: 'bing_image', expect: { } +}); +var rpcUnsplash = rpc.declare({ + object: 'luci.shadcnui', method: 'unsplash_image', + params: [ 'query' ], expect: { } +}); +var rpcWallhaven = rpc.declare({ + object: 'luci.shadcnui', method: 'wallhaven_image', + params: [ 'query' ], expect: { } +}); + +/* Helpers ------------------------------------------------------------------ */ + +function hexToHsl(hex) { + hex = (hex || '').replace('#', ''); + if (hex.length == 3) hex = hex.split('').map(function (c) { return c + c; }).join(''); + if (hex.length != 6) return null; + var r = parseInt(hex.slice(0, 2), 16) / 255; + var g = parseInt(hex.slice(2, 4), 16) / 255; + var b = parseInt(hex.slice(4, 6), 16) / 255; + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2; + if (max == min) { h = 0; s = 0; } + else { + var d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return (h * 360).toFixed(1) + ' ' + (s * 100).toFixed(1) + '% ' + (l * 100).toFixed(1) + '%'; +} + +function hslToHex(hsl) { + if (!hsl) return '#000000'; + var m = hsl.match(/(-?[\d.]+)\s+(-?[\d.]+)%\s+(-?[\d.]+)%/); + if (!m) return '#000000'; + var h = +m[1] / 360, s = +m[2] / 100, l = +m[3] / 100; + function f(p, q, t) { + if (t < 0) t += 1; + if (t > 1) t -= 1; + if (t < 1 / 6) return p + (q - p) * 6 * t; + if (t < 1 / 2) return q; + if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; + return p; + } + var r, g, b; + if (s == 0) { r = g = b = l; } + else { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = f(p, q, h + 1 / 3); g = f(p, q, h); b = f(p, q, h - 1 / 3); + } + function toHex(x) { + var v = Math.round(x * 255).toString(16); + return v.length == 1 ? '0' + v : v; + } + return '#' + toHex(r) + toHex(g) + toHex(b); +} + +/* Live preview applied directly to while user is editing ------------- */ +function applyPreview(state) { + var root = document.documentElement; + root.setAttribute('data-base', state.base || 'zinc'); + if (state.mode === 'system') { + var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + root.setAttribute('data-theme', prefersDark ? 'dark' : 'light'); + } else { + root.setAttribute('data-theme', state.mode === 'dark' ? 'dark' : 'light'); + } + root.setAttribute('data-mode', state.mode || 'system'); + if (state.accent) { + root.style.setProperty('--primary', state.accent); + root.style.setProperty('--ring', state.accent); + } else { + root.style.removeProperty('--primary'); + root.style.removeProperty('--ring'); + } + if (state.radius) root.style.setProperty('--radius', state.radius); +} + +function setBgPreview(url) { + var img = document.getElementById('shadcnui-bg-preview'); + if (img) img.style.backgroundImage = url ? 'url("' + url + '")' : 'none'; +} + +/* View definition ---------------------------------------------------------- */ + +return view.extend({ + + load: function () { return rpcList(); }, + + render: function (cfg) { + cfg = cfg || {}; + var main = cfg.main || {}; + var unsplashCfg = cfg.unsplash || {}; + var wallhavenCfg = cfg.wallhaven || {}; + + // initial state + var state = { + mode: main.mode || 'system', + base: main.base || 'zinc', + accent: main.accent || '', + radius: main.radius || '0.5rem', + bg_source: main.bg_source || 'none', + bg_url: main.bg_url || '', + unsplash_key: '', + wallhaven_key: '' + }; + + // --- Builders ----------------------------------------------------------- + + function row(label, descr, control) { + var d = E('div', { 'class': 'cbi-value' }, [ + E('div', { 'class': 'cbi-value-title' }, label), + E('div', { 'class': 'cbi-value-field' }, [ control ]) + ]); + if (descr) d.appendChild(E('div', { 'class': 'cbi-value-description' }, descr)); + return d; + } + + function select(name, options, value) { + var sel = E('select', { 'id': 'sci-' + name }); + options.forEach(function (o) { + var opt = E('option', { 'value': o[0] }, o[1]); + if (o[0] === value) opt.selected = true; + sel.appendChild(opt); + }); + return sel; + } + + var modeSel = select('mode', [ + [ 'system', _('Follow system') ], + [ 'light', _('Light') ], + [ 'dark', _('Dark') ] + ], state.mode); + modeSel.addEventListener('change', function (ev) { + state.mode = ev.target.value; + applyPreview(state); + }); + + var baseSel = select('base', [ + [ 'zinc', 'Zinc' ], + [ 'slate', 'Slate' ], + [ 'stone', 'Stone' ], + [ 'gray', 'Gray' ], + [ 'neutral', 'Neutral' ] + ], state.base); + baseSel.addEventListener('change', function (ev) { + state.base = ev.target.value; + applyPreview(state); + }); + + // Accent: color picker + readout of the HSL string + var accentPick = E('input', { + type: 'color', + value: state.accent ? hslToHex(state.accent) : '#18181b', + style: 'width:42px;height:36px;padding:2px;cursor:pointer' + }); + var accentText = E('input', { + type: 'text', + placeholder: 'e.g. 240 5.9% 10% (leave empty for base default)', + value: state.accent, + style: 'flex:1' + }); + var accentReset = E('button', { + 'class': 'btn btn-outline btn-sm', + 'type': 'button', + 'click': function () { + state.accent = ''; accentText.value = ''; + applyPreview(state); + } + }, _('Reset')); + var accentRow = E('div', { style: 'display:flex;gap:.5rem;align-items:center' }, + [ accentPick, accentText, accentReset ]); + + accentPick.addEventListener('input', function (ev) { + var hsl = hexToHsl(ev.target.value); + state.accent = hsl; + accentText.value = hsl; + applyPreview(state); + }); + accentText.addEventListener('change', function (ev) { + state.accent = ev.target.value.trim(); + if (state.accent) accentPick.value = hslToHex(state.accent); + applyPreview(state); + }); + + // Radius slider + var radiusInp = E('input', { + type: 'range', min: '0', max: '1.5', step: '0.05', + value: parseFloat(state.radius) || 0.5, + style: 'flex:1' + }); + var radiusOut = E('span', { style: 'width:4em;text-align:right;font-variant-numeric:tabular-nums' }, + state.radius); + radiusInp.addEventListener('input', function (ev) { + state.radius = ev.target.value + 'rem'; + radiusOut.textContent = state.radius; + applyPreview(state); + }); + var radiusRow = E('div', { style: 'display:flex;gap:.75rem;align-items:center' }, + [ radiusInp, radiusOut ]); + + // Background source selector + per-source UI ---------------------------- + + var bgSrcSel = select('bg_source', [ + [ 'none', _('None') ], + [ 'bing', _('Bing daily') ], + [ 'url', _('Custom URL') ], + [ 'local', _('Local upload') ], + [ 'unsplash', _('Unsplash') ], + [ 'wallhaven', _('Wallhaven') ] + ], state.bg_source); + + var bgUrlInp = E('input', { + type: 'url', + value: state.bg_url, + placeholder: 'https://example.com/wallpaper.jpg' + }); + bgUrlInp.addEventListener('change', function (ev) { + state.bg_url = ev.target.value.trim(); + setBgPreview(state.bg_url); + }); + + var unsplashKeyInp = E('input', { + type: 'password', placeholder: '***', + value: unsplashCfg.access_key === '***' ? '' : (unsplashCfg.access_key || '') + }); + unsplashKeyInp.addEventListener('change', function (ev) { + state.unsplash_key = ev.target.value; + }); + + var unsplashQueryInp = E('input', { + type: 'text', + value: unsplashCfg.query || 'nature', + placeholder: 'nature, mountains, abstract...' + }); + + var wallhavenKeyInp = E('input', { + type: 'password', placeholder: '*** (optional, for SFW token)', + value: wallhavenCfg.api_key === '***' ? '' : (wallhavenCfg.api_key || '') + }); + wallhavenKeyInp.addEventListener('change', function (ev) { + state.wallhaven_key = ev.target.value; + }); + + var wallhavenQueryInp = E('input', { + type: 'text', + value: wallhavenCfg.query || 'minimal', + placeholder: 'minimal, cyberpunk, anime...' + }); + + // Fetch buttons + function feedback(msg, kind) { + ui.addNotification(null, E('p', {}, msg), kind || 'info'); + } + + var bingFetchBtn = E('button', { + 'class': 'btn', + 'type': 'button', + 'click': function () { + bingFetchBtn.disabled = true; + rpcBing().then(function (r) { + bingFetchBtn.disabled = false; + if (r && r.url) { + state.bg_url = r.url; + bgUrlInp.value = r.url; + setBgPreview(r.url); + feedback(_('Bing image loaded.'), 'success'); + } else { + feedback(_('Failed to fetch Bing image.'), 'danger'); + } + }); + } + }, _('Fetch today\'s Bing image')); + + var unsplashFetchBtn = E('button', { + 'class': 'btn', + 'type': 'button', + 'click': function () { + unsplashFetchBtn.disabled = true; + rpcUnsplash(unsplashQueryInp.value).then(function (r) { + unsplashFetchBtn.disabled = false; + if (r && r.url) { + state.bg_url = r.url; + bgUrlInp.value = r.url; + setBgPreview(r.url); + feedback(_('Unsplash image loaded' + (r.attribution ? ' — ' + r.attribution : '')) + '.', 'success'); + } else { + feedback(_('Failed to fetch Unsplash image.') + (r && r.error ? ' (' + r.error + ')' : ''), 'danger'); + } + }); + } + }, _('Fetch random Unsplash')); + + var wallhavenFetchBtn = E('button', { + 'class': 'btn', + 'type': 'button', + 'click': function () { + wallhavenFetchBtn.disabled = true; + rpcWallhaven(wallhavenQueryInp.value).then(function (r) { + wallhavenFetchBtn.disabled = false; + if (r && r.url) { + state.bg_url = r.url; + bgUrlInp.value = r.url; + setBgPreview(r.url); + feedback(_('Wallhaven image loaded.'), 'success'); + } else { + feedback(_('Failed to fetch Wallhaven image.') + (r && r.error ? ' (' + r.error + ')' : ''), 'danger'); + } + }); + } + }, _('Fetch random Wallhaven')); + + // Local file upload — write into /www/luci-static/shadcnui/img/background/ + var localUpload = E('input', { type: 'file', accept: 'image/*' }); + localUpload.addEventListener('change', function (ev) { + var f = ev.target.files && ev.target.files[0]; + if (!f) return; + // strip path components, allow only safe chars in the filename + var safe = f.name.replace(/[^A-Za-z0-9._-]/g, '_'); + var path = '/www/luci-static/shadcnui/img/background/' + safe; + var reader = new FileReader(); + reader.onload = function () { + var bytes = new Uint8Array(reader.result); + fs.write(path, bytes).then(function () { + var publicUrl = '/luci-static/shadcnui/img/background/' + safe; + state.bg_url = publicUrl; + bgUrlInp.value = publicUrl; + setBgPreview(publicUrl); + feedback(_('Uploaded to %s').format(path), 'success'); + }).catch(function (err) { + feedback(_('Upload failed: %s').format(err.message || err), 'danger'); + }); + }; + reader.readAsArrayBuffer(f); + }); + + // Containers per source — show/hide based on selector + var bgSections = { + none: E('p', { 'class': 'cbi-value-description' }, + _('No background image will be shown on the login page.')), + url: E('div', { style: 'display:grid;gap:.5rem' }, [ bgUrlInp ]), + local: E('div', { style: 'display:grid;gap:.5rem' }, [ + localUpload, + E('p', { 'class': 'cbi-value-description' }, + _('Files are stored under /www/luci-static/shadcnui/img/background/.')) + ]), + bing: E('div', { style: 'display:grid;gap:.5rem' }, [ + bingFetchBtn, + E('p', { 'class': 'cbi-value-description' }, + _('Requires internet access from the router.')) + ]), + unsplash: E('div', { style: 'display:grid;gap:.5rem' }, [ + row(_('Access Key'), _('Get a free Demo key at unsplash.com/developers.'), + unsplashKeyInp), + row(_('Query'), null, unsplashQueryInp), + unsplashFetchBtn + ]), + wallhaven: E('div', { style: 'display:grid;gap:.5rem' }, [ + row(_('API Key'), _('Optional. Required only for NSFW or rate-limit relief.'), + wallhavenKeyInp), + row(_('Query'), null, wallhavenQueryInp), + wallhavenFetchBtn + ]) + }; + + function showBgSection() { + Object.keys(bgSections).forEach(function (k) { + bgSections[k].style.display = (k === state.bg_source) ? '' : 'none'; + }); + } + + bgSrcSel.addEventListener('change', function (ev) { + state.bg_source = ev.target.value; + showBgSection(); + }); + + var bgPreview = E('div', { + 'id': 'shadcnui-bg-preview', + 'style': 'width:100%;aspect-ratio:16/6;border-radius:var(--radius);' + + 'border:1px solid hsl(var(--border));background-size:cover;' + + 'background-position:center;background-color:hsl(var(--muted))' + }); + setBgPreview(state.bg_url); + + // --- Save button -------------------------------------------------------- + + var saveBtn = E('button', { + 'class': 'btn btn-lg', + 'type': 'button', + 'click': function () { + saveBtn.disabled = true; + rpcSave(state.mode, state.base, state.accent, state.radius, + state.bg_source, state.bg_url, + state.unsplash_key, state.wallhaven_key).then(function (ok) { + saveBtn.disabled = false; + if (ok) { + ui.addNotification(null, + E('p', {}, _('Settings saved. Reload the page to apply everywhere.')), + 'success'); + } else { + ui.addNotification(null, + E('p', {}, _('Failed to save settings.')), 'danger'); + } + }); + } + }, _('Save')); + + // --- Layout ------------------------------------------------------------- + + showBgSection(); + applyPreview(state); + + return E('div', {}, [ + E('h1', { 'class': 'page-title' }, _('Theme')), + E('p', { 'class': 'page-description' }, + _('Personalize the shadcnui LuCI theme. Changes preview live; click Save to persist.')), + + E('div', { 'class': 'cbi-section' }, [ + E('legend', {}, _('Appearance')), + row(_('Mode'), + _('Light, dark, or follow your operating system.'), + modeSel), + row(_('Base palette'), + _('Neutral palette — same options as on ui.shadcn.com.'), + baseSel), + row(_('Accent color'), + _('Overrides --primary. Hex picker writes the equivalent HSL string used by shadcn tokens.'), + accentRow), + row(_('Border radius'), + _('Controls corner roundness across the theme.'), + radiusRow) + ]), + + E('div', { 'class': 'cbi-section' }, [ + E('legend', {}, _('Login background')), + row(_('Source'), null, bgSrcSel), + bgSections.none, + bgSections.url, + bgSections.local, + bgSections.bing, + bgSections.unsplash, + bgSections.wallhaven, + E('div', { style: 'margin-top:1rem' }, bgPreview) + ]), + + E('div', { 'class': 'cbi-page-actions' }, [ saveBtn ]) + ]); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-shadcnui-config/root/etc/uci-defaults/40_luci-app-shadcnui-config b/luci-app-shadcnui-config/root/etc/uci-defaults/40_luci-app-shadcnui-config new file mode 100755 index 000000000..2ce863fdf --- /dev/null +++ b/luci-app-shadcnui-config/root/etc/uci-defaults/40_luci-app-shadcnui-config @@ -0,0 +1,17 @@ +#!/bin/sh + +# Ensure the local-upload target directory exists & is writable by rpcd's user. +mkdir -p /www/luci-static/shadcnui/img/background +chmod 0755 /www/luci-static/shadcnui/img/background + +# Make our RPC handler executable in case Makefile permissions did not stick. +[ -f /usr/libexec/rpcd/luci.shadcnui ] && chmod 0755 /usr/libexec/rpcd/luci.shadcnui + +# Tell rpcd to re-scan handlers and ACLs. +killall -HUP rpcd 2>/dev/null +/etc/init.d/rpcd reload 2>/dev/null + +# Drop LuCI's menu / ACL caches so the new entry shows up immediately. +rm -f /tmp/luci-indexcache /tmp/luci-modulecache/* 2>/dev/null + +exit 0 diff --git a/luci-app-shadcnui-config/root/usr/libexec/rpcd/luci.shadcnui b/luci-app-shadcnui-config/root/usr/libexec/rpcd/luci.shadcnui new file mode 100755 index 000000000..8febc555f --- /dev/null +++ b/luci-app-shadcnui-config/root/usr/libexec/rpcd/luci.shadcnui @@ -0,0 +1,197 @@ +#!/bin/sh +# RPC handler for luci-app-shadcnui-config +# Registered under /usr/libexec/rpcd/luci.shadcnui and invoked by rpcd via stdio. +# +# Methods: +# list — return current UCI config (with secret keys masked) +# bing_image — fetch today's Bing wallpaper URL +# unsplash_image — fetch a random Unsplash photo URL (requires access_key) +# wallhaven_image — fetch a random Wallhaven image URL +# save — write user-supplied options back into /etc/config/shadcnui + +. /usr/share/libubox/jshn.sh + +case "$1" in + list) + json_init + json_add_object "list"; json_close_object + json_add_object "bing_image"; json_close_object + json_add_object "unsplash_image" + json_add_object "args"; json_add_string query ""; json_close_object + json_close_object + json_add_object "wallhaven_image" + json_add_object "args"; json_add_string query ""; json_close_object + json_close_object + json_add_object "save" + json_add_object "args" + json_add_string mode "" + json_add_string base "" + json_add_string accent "" + json_add_string radius "" + json_add_string bg_source "" + json_add_string bg_url "" + json_add_string unsplash_key "" + json_add_string wallhaven_key "" + json_close_object + json_close_object + json_dump + ;; + + call) + method="$2" + read -r input + + case "$method" in + list) + json_init + json_add_object "config" + json_add_object "main" + for opt in mode base accent radius bg_source bg_url; do + v=$(uci -q get shadcnui.main."$opt") + json_add_string "$opt" "$v" + done + json_close_object + json_add_object "bing" + for opt in enabled market cache_ttl; do + v=$(uci -q get shadcnui.bing."$opt") + json_add_string "$opt" "$v" + done + json_close_object + json_add_object "unsplash" + v=$(uci -q get shadcnui.unsplash.enabled); json_add_string enabled "$v" + v=$(uci -q get shadcnui.unsplash.query); json_add_string query "$v" + k=$(uci -q get shadcnui.unsplash.access_key) + if [ -n "$k" ]; then + json_add_string access_key "***" + else + json_add_string access_key "" + fi + json_close_object + json_add_object "wallhaven" + v=$(uci -q get shadcnui.wallhaven.enabled); json_add_string enabled "$v" + v=$(uci -q get shadcnui.wallhaven.query); json_add_string query "$v" + v=$(uci -q get shadcnui.wallhaven.purity); json_add_string purity "$v" + v=$(uci -q get shadcnui.wallhaven.categories); json_add_string categories "$v" + k=$(uci -q get shadcnui.wallhaven.api_key) + if [ -n "$k" ]; then + json_add_string api_key "***" + else + json_add_string api_key "" + fi + json_close_object + json_close_object + json_dump + ;; + + bing_image) + body=$(curl -fsSL --max-time 10 \ + 'https://www.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1&mkt=en-US' 2>/dev/null) + path=$(echo "$body" | jsonfilter -e '@.images[0].url' 2>/dev/null) + json_init + if [ -n "$path" ]; then + json_add_string "url" "https://www.bing.com${path}" + else + json_add_string "error" "bing_fetch_failed" + fi + json_dump + ;; + + unsplash_image) + query=$(echo "$input" | jsonfilter -e '@.query' 2>/dev/null) + [ -z "$query" ] && query=$(uci -q get shadcnui.unsplash.query) + [ -z "$query" ] && query="nature" + key=$(uci -q get shadcnui.unsplash.access_key) + json_init + if [ -z "$key" ]; then + json_add_string "error" "no_access_key" + else + body=$(curl -fsSL --max-time 10 \ + -H "Authorization: Client-ID $key" \ + "https://api.unsplash.com/photos/random?orientation=landscape&query=$query" 2>/dev/null) + url=$(echo "$body" | jsonfilter -e '@.urls.regular' 2>/dev/null) + author=$(echo "$body" | jsonfilter -e '@.user.name' 2>/dev/null) + if [ -n "$url" ]; then + json_add_string "url" "$url" + json_add_string "attribution" "$author" + else + json_add_string "error" "unsplash_fetch_failed" + fi + fi + json_dump + ;; + + wallhaven_image) + query=$(echo "$input" | jsonfilter -e '@.query' 2>/dev/null) + [ -z "$query" ] && query=$(uci -q get shadcnui.wallhaven.query) + [ -z "$query" ] && query="minimal" + purity=$(uci -q get shadcnui.wallhaven.purity); [ -z "$purity" ] && purity="100" + cats=$(uci -q get shadcnui.wallhaven.categories); [ -z "$cats" ] && cats="111" + key=$(uci -q get shadcnui.wallhaven.api_key) + url="https://wallhaven.cc/api/v1/search?q=${query}&purity=${purity}&categories=${cats}&sorting=random&atleast=1920x1080" + if [ -n "$key" ]; then + body=$(curl -fsSL --max-time 10 -H "X-API-Key: $key" "$url" 2>/dev/null) + else + body=$(curl -fsSL --max-time 10 "$url" 2>/dev/null) + fi + pick=$(echo "$body" | jsonfilter -e '@.data[0].path' 2>/dev/null) + pid=$(echo "$body" | jsonfilter -e '@.data[0].id' 2>/dev/null) + json_init + if [ -n "$pick" ]; then + json_add_string "url" "$pick" + json_add_string "attribution" "wallhaven.cc/w/$pid" + else + json_add_string "error" "wallhaven_fetch_failed" + fi + json_dump + ;; + + save) + mode=$(echo "$input" | jsonfilter -e '@.mode' 2>/dev/null) + base=$(echo "$input" | jsonfilter -e '@.base' 2>/dev/null) + accent=$(echo "$input" | jsonfilter -e '@.accent' 2>/dev/null) + radius=$(echo "$input" | jsonfilter -e '@.radius' 2>/dev/null) + bg_source=$(echo "$input" | jsonfilter -e '@.bg_source' 2>/dev/null) + bg_url=$(echo "$input" | jsonfilter -e '@.bg_url' 2>/dev/null) + uk=$(echo "$input" | jsonfilter -e '@.unsplash_key' 2>/dev/null) + wk=$(echo "$input" | jsonfilter -e '@.wallhaven_key' 2>/dev/null) + + case "$mode" in system|light|dark) + uci -q set shadcnui.main.mode="$mode" ;; + esac + case "$base" in zinc|slate|stone|gray|neutral) + uci -q set shadcnui.main.base="$base" ;; + esac + case "$bg_source" in none|bing|unsplash|wallhaven|url|local) + uci -q set shadcnui.main.bg_source="$bg_source" ;; + esac + + sanitized=$(echo "$accent" | tr -cd '0-9a-zA-Z .%#-') + uci -q set shadcnui.main.accent="$sanitized" + + sanitized_r=$(echo "$radius" | tr -cd '0-9a-zA-Z .%-') + uci -q set shadcnui.main.radius="$sanitized_r" + + uci -q set shadcnui.main.bg_url="$bg_url" + + if [ -n "$uk" ] && [ "$uk" != "***" ]; then + uci -q set shadcnui.unsplash.access_key="$uk" + fi + if [ -n "$wk" ] && [ "$wk" != "***" ]; then + uci -q set shadcnui.wallhaven.api_key="$wk" + fi + + uci -q commit shadcnui + + json_init + json_add_boolean "ok" 1 + json_dump + ;; + + *) + json_init + json_add_string "error" "unknown_method" + json_dump + ;; + esac + ;; +esac diff --git a/luci-app-shadcnui-config/root/usr/share/luci/menu.d/luci-app-shadcnui-config.json b/luci-app-shadcnui-config/root/usr/share/luci/menu.d/luci-app-shadcnui-config.json new file mode 100644 index 000000000..3a1efb770 --- /dev/null +++ b/luci-app-shadcnui-config/root/usr/share/luci/menu.d/luci-app-shadcnui-config.json @@ -0,0 +1,13 @@ +{ + "admin/system/shadcnui-config": { + "title": "Theme", + "order": 80, + "action": { + "type": "view", + "path": "shadcnui/config" + }, + "depends": { + "acl": [ "luci-app-shadcnui-config" ] + } + } +} diff --git a/luci-app-shadcnui-config/root/usr/share/rpcd/acl.d/luci-app-shadcnui-config.json b/luci-app-shadcnui-config/root/usr/share/rpcd/acl.d/luci-app-shadcnui-config.json new file mode 100644 index 000000000..8a48e465d --- /dev/null +++ b/luci-app-shadcnui-config/root/usr/share/rpcd/acl.d/luci-app-shadcnui-config.json @@ -0,0 +1,23 @@ +{ + "luci-app-shadcnui-config": { + "description": "Grant access to shadcnui theme configuration", + "read": { + "uci": [ "shadcnui" ], + "file": { + "/www/luci-static/shadcnui/img/background/": [ "list" ] + }, + "ubus": { + "luci.shadcnui": [ "*" ] + } + }, + "write": { + "uci": [ "shadcnui" ], + "file": { + "/www/luci-static/shadcnui/img/background/": [ "write" ] + }, + "ubus": { + "luci.shadcnui": [ "*" ] + } + } + } +} diff --git a/luci-app-vantage/Makefile b/luci-app-vantage/Makefile new file mode 100644 index 000000000..7c6577a87 --- /dev/null +++ b/luci-app-vantage/Makefile @@ -0,0 +1,49 @@ +# +# Vantage dashboard for LuCI: a live, read-only overview of the device, +# its uplink, radios, SSIDs and stations. The only write is the station +# alias, stored in /etc/config/vantage (own ACL group). +# +# This is free software, licensed under the GNU General Public License, +# version 3 or (at your option) any later version. +# + +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-app-vantage +PKG_VERSION:=1.0.1 +PKG_RELEASE:=1 +PKG_LICENSE:=GPL-3.0-or-later + +LUCI_TITLE:=Vantage dashboard +LUCI_DESCRIPTION:=Live network dashboard (landing page under admin/dashboard): device, \ + uplink, radios, SSIDs and stations with health and experience scores. \ + Read-only except for station names, which are kept in /etc/config/vantage. +# rpcd-mod-iwinfo provides the station lists (iwinfo info/assoclist). +# rpcd-mod-ucode runs the app's plugin (/usr/share/rpcd/ucode/luci.vantage: +# key-free wireless status, validated alias writes), which imports the uci +# and ubus ucode modules; luci-base pulls all three in already, they are +# listed because the plugin needs them. +# Optional at runtime, degrade when absent: rpcd-mod-rrdns (reverse DNS +# names), umdns (mDNS names), hostapd ubus objects (WPS names, client +# capabilities). +LUCI_DEPENDS:=+luci-base +rpcd +rpcd-mod-iwinfo +rpcd-mod-ucode +ucode-mod-uci +ucode-mod-ubus +LUCI_PKGARCH:=all +LUCI_MAINTAINER:=anothaDev <10740588+anothaDev@users.noreply.github.com> +LUCI_URL:=https://github.com/anothaDev/luci-theme-vantage + +# app.css uses custom properties, color-mix() and container queries, +# which csstidy (a CSS2-era minifier) mangles; ship it byte for byte. +LUCI_MINIFY_CSS:=0 + +# Language packages (po//vantage.po) are luci-i18n-vantage- +# (luci.mk's default name) and carry the package version instead of one +# derived from git history. +PKG_PO_VERSION:=$(PKG_VERSION)-r$(PKG_RELEASE) + +define Package/luci-app-vantage/conffiles +/etc/config/vantage +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css new file mode 100644 index 000000000..a13d0ffe5 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/app.css @@ -0,0 +1,1094 @@ +/* + * Vantage dashboard (luci-app-vantage) + * + * Styled only through the shared theme tokens (--v-*), each with a + * fallback so the app also reads well under themes that do not define + * them. Everything is scoped under .vt-app; class names use the vt- + * prefix and avoid the names the Vantage theme shell uses. + * + * 1. Tokens 5. Hero: network path 9. Clients table + * 2. Base 6. System 10. Talkers, SSIDs + * 3. Layout 7. Chart 11. Drawer + * 4. Head, health 8. Radios 12. Small screens, motion + */ + +/* ------------------------------------------------------------ 1. tokens */ + +.vt-app { + --vt-bg: var(--v-bg, #f3f4f6); + --vt-surface: var(--v-surface, #ffffff); + --vt-surface-2: var(--v-surface-2, #f6f7f9); + --vt-surface-3: var(--v-surface-3, #eceef1); + --vt-line: var(--v-line, #e2e5e9); + --vt-line-2: var(--v-line-2, #cbd0d6); + --vt-text: var(--v-text, #15181c); + --vt-text-2: var(--v-text-2, #4b525b); + --vt-text-3: var(--v-text-3, #6a717a); + --vt-accent: var(--v-accent, #3563e9); + --vt-accent-ink: var(--v-accent-ink, #ffffff); + --vt-accent-soft: var(--v-accent-soft, rgba(53, 99, 233, .12)); + --vt-ok: var(--v-ok, #0c7f63); + --vt-ok-soft: var(--v-ok-soft, rgba(12, 127, 99, .11)); + --vt-warn: var(--v-warn, #9a5b00); + --vt-warn-soft: var(--v-warn-soft, rgba(214, 138, 0, .15)); + --vt-err: var(--v-err, #c42b2b); + --vt-err-soft: var(--v-err-soft, rgba(196, 43, 43, .1)); + --vt-info: var(--v-info, #1f63c6); + --vt-info-soft: var(--v-info-soft, rgba(31, 99, 198, .1)); + --vt-font: var(--v-font, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif); + --vt-mono: var(--v-mono, ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace); + --vt-r-sm: var(--v-r-sm, 6px); + --vt-r: var(--v-r, 10px); + --vt-r-lg: var(--v-r-lg, 14px); + --vt-shadow: var(--v-shadow, 0 1px 2px rgba(16, 20, 24, .05), 0 1px 1px rgba(16, 20, 24, .03)); + + /* derived: readable accent for text/strokes on surfaces (a lime or + vermilion accent is a fill colour, not a text colour) */ + --vt-accent-text: color-mix(in oklab, var(--vt-accent) 58%, var(--vt-text)); + --vt-accent-line: color-mix(in oklab, var(--vt-accent) 78%, var(--vt-text)); + /* charts: the theme's categorical chart palette (--v-chart-*), in + fixed order: uplink, 2.4 GHz, 5 GHz, 6 GHz, CPU, spare */ + --vt-chart-1: var(--v-chart-1, #6f9c00); + --vt-chart-2: var(--v-chart-2, #2a78d6); + --vt-chart-3: var(--v-chart-3, #e0632f); + --vt-chart-4: var(--v-chart-4, #0f9fb0); + --vt-chart-5: var(--v-chart-5, #8b5cf6); + --vt-chart-6: var(--v-chart-6, #d55181); + --vt-chart-grid: var(--v-chart-grid, color-mix(in srgb, var(--vt-text) 8%, transparent)); + --vt-chart-axis: var(--v-chart-axis, var(--vt-text-3)); + --vt-fill-op: var(--v-chart-fill-opacity, .22); + /* bands: identity colours, the same everywhere (badges, tiles, charts) */ + --vt-b2: var(--vt-chart-2); + --vt-b5: var(--vt-chart-3); + --vt-b6: var(--vt-chart-4); + --vt-b60: var(--vt-text-2); + --vt-series-up: var(--vt-chart-1); + --vt-mem: var(--vt-info); + --vt-hover: color-mix(in srgb, var(--vt-surface-2) 70%, var(--vt-surface-3)); + --vt-av-l: 92%; + --vt-av-s: 55%; + --vt-av-ink-l: 34%; + --vt-scrim: rgba(12, 14, 17, .32); + --vt-sheet-shadow: -18px 0 48px rgba(12, 14, 17, .16), -2px 0 8px rgba(12, 14, 17, .06); +} + +html[data-darkmode="true"] .vt-app { + --vt-bg: var(--v-bg, #0f1113); + --vt-surface: var(--v-surface, #171a1d); + --vt-surface-2: var(--v-surface-2, #1d2024); + --vt-surface-3: var(--v-surface-3, #262a2f); + --vt-line: var(--v-line, #2a2e33); + --vt-line-2: var(--v-line-2, #3a4047); + --vt-text: var(--v-text, #e8eaed); + --vt-text-2: var(--v-text-2, #b4bac2); + --vt-text-3: var(--v-text-3, #8c939c); + --vt-accent: var(--v-accent, #7aa2ff); + --vt-accent-ink: var(--v-accent-ink, #0b1020); + --vt-accent-soft: var(--v-accent-soft, rgba(122, 162, 255, .14)); + --vt-ok: var(--v-ok, #3ccf9f); + --vt-ok-soft: var(--v-ok-soft, rgba(60, 207, 159, .13)); + --vt-warn: var(--v-warn, #f0b54a); + --vt-warn-soft: var(--v-warn-soft, rgba(240, 181, 74, .14)); + --vt-err: var(--v-err, #ff7a70); + --vt-err-soft: var(--v-err-soft, rgba(255, 122, 112, .14)); + --vt-info: var(--v-info, #6aa8ff); + --vt-info-soft: var(--v-info-soft, rgba(106, 168, 255, .14)); + --vt-shadow: var(--v-shadow, 0 1px 0 rgba(255, 255, 255, .03) inset, 0 1px 2px rgba(0, 0, 0, .3)); + --vt-accent-text: color-mix(in oklab, var(--vt-accent) 72%, var(--vt-text)); + --vt-accent-line: var(--vt-accent); + --vt-chart-1: var(--v-chart-1, #c8f031); + --vt-chart-2: var(--v-chart-2, #3f8fe8); + --vt-chart-3: var(--v-chart-3, #de6d33); + --vt-chart-4: var(--v-chart-4, #1aa6b8); + --vt-chart-5: var(--v-chart-5, #9a7cf2); + --vt-chart-6: var(--v-chart-6, #d9588c); + --vt-chart-grid: var(--v-chart-grid, color-mix(in srgb, var(--vt-text) 7%, transparent)); + --vt-fill-op: var(--v-chart-fill-opacity, .2); + --vt-av-l: 24%; + --vt-av-s: 30%; + --vt-av-ink-l: 76%; + --vt-scrim: rgba(0, 0, 0, .5); + --vt-sheet-shadow: -18px 0 48px rgba(0, 0, 0, .5); +} + +/* -------------------------------------------------------------- 2. base */ + +.vt-app .vt-body { container: vt / inline-size; } +.vt-app { + color: var(--vt-text); + font-family: var(--vt-font); + font-size: 13.5px; + line-height: 1.45; + font-variant-numeric: tabular-nums; + -webkit-font-smoothing: antialiased; +} +.vt-app *, .vt-app *::before, .vt-app *::after { box-sizing: border-box; } +.vt-app [hidden] { display: none !important; } +.vt-app h2, .vt-app h3, .vt-app h4, .vt-app p, .vt-app dl, .vt-app dd, .vt-app ul { margin: 0; padding: 0; } +.vt-app ul { list-style: none; } +/* Bare elements some themes style globally (bootstrap: header is a sticky + dark navbar with 40px paragraphs, footer a flex row with a top rule, + headings sit in 36px line boxes, labels get a fixed font). :where() + keeps these at class specificity, so every rule of the app below still + wins and only the theme's element selectors are overridden. */ +.vt-app :where(header, footer) { + position: static; z-index: auto; display: block; overflow: visible; + margin: 0; padding: 0; border: 0; border-radius: 0; + background: none; box-shadow: none; color: inherit; text-shadow: none; +} +.vt-app :where(header, footer) :where(p, a, span) { line-height: inherit; text-shadow: none; } +.vt-app :where(header) :where(a) { color: inherit; } +.vt-app :where(h2, h3, h4) { line-height: 1.25; font-family: inherit; } +.vt-app :where(label) { font: inherit; line-height: inherit; } +.vt-app button { + /* themes style bare buttons (centred, fixed height, shadows); the + dashboard's buttons are layout elements with their own classes */ + font: inherit; color: inherit; letter-spacing: normal; text-transform: none; + justify-content: flex-start; align-items: center; text-align: left; + height: auto; min-height: 0; white-space: normal; user-select: auto; vertical-align: baseline; +} +.vt-app .vt-mono { font-family: var(--vt-mono); font-size: .92em; letter-spacing: -.01em; } +.vt-app .vt-tn { font-variant-numeric: tabular-nums; } +.vt-app .vt-muted { color: var(--vt-text-3); } +.vt-app .vt-hint { color: var(--vt-text-3); font-size: 12px; } +.vt-app .vt-t-warn { color: var(--vt-warn); } +.vt-app .vt-t-err { color: var(--vt-err); } +.vt-app .vt-label { display: block; color: var(--vt-text-3); font-size: 11.5px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; } +.vt-app .vt-big { font-size: 30px; font-weight: 680; letter-spacing: -.02em; line-height: 1.05; } + +.vt-app .vt-ico { + width: 18px; height: 18px; flex: none; + fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; +} +.vt-app .vt-ico-sm { width: 14px; height: 14px; } + +.vt-app :focus-visible { + outline: 2px solid var(--vt-accent-line); + outline-offset: 2px; +} + +/* buttons of the app (the theme's global button look is reset here) */ +.vt-app .vt-btn { + display: inline-flex; align-items: center; gap: 6px; + height: 32px; padding: 0 12px; + border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); + background: var(--vt-surface); color: var(--vt-text); + font-size: 13px; font-weight: 560; line-height: 1; + cursor: pointer; box-shadow: none; text-transform: none; min-width: 0; + transition: background .15s, border-color .15s; +} +.vt-app .vt-btn:hover { background: var(--vt-surface-2); border-color: var(--vt-text-3); } +.vt-app .vt-btn[disabled] { opacity: .55; cursor: progress; } +.vt-app .vt-btn.vt-btn-primary { background: var(--vt-accent); border-color: var(--vt-accent); color: var(--vt-accent-ink); } +.vt-app .vt-btn.vt-btn-primary:hover { filter: brightness(1.05); } +.vt-app .vt-btn.vt-btn-ghost { border-color: transparent; background: transparent; color: var(--vt-text-2); padding: 0 8px; } +.vt-app .vt-btn.vt-btn-ghost:hover { background: var(--vt-surface-3); color: var(--vt-text); } + +.vt-app .vt-link { + display: inline-flex; align-items: center; gap: 2px; + color: var(--vt-accent-text); font-size: 12.5px; font-weight: 600; text-decoration: none; white-space: nowrap; +} +.vt-app .vt-link .vt-ico { width: 14px; height: 14px; transition: transform .15s; } +.vt-app .vt-link:hover { text-decoration: none; color: var(--vt-text); } +.vt-app .vt-link:hover .vt-ico { transform: translateX(2px); } + +.vt-app .vt-pill { + display: inline-flex; align-items: center; height: 18px; padding: 0 6px; + border-radius: 999px; font-size: 10.5px; font-weight: 650; letter-spacing: .02em; white-space: nowrap; + background: var(--vt-surface-3); color: var(--vt-text-2); +} +.vt-app .vt-pill-accent { background: var(--vt-accent); color: var(--vt-accent-ink); } +.vt-app .vt-pill-warn { background: var(--vt-warn-soft); color: var(--vt-warn); } + +.vt-app .vt-band { + display: inline-flex; align-items: center; height: 18px; padding: 0 6px; + border-radius: 5px; font-size: 10.5px; font-weight: 700; letter-spacing: .01em; white-space: nowrap; + --vt-bc: var(--vt-b60); + background: color-mix(in oklab, var(--vt-bc) 14%, transparent); + color: color-mix(in oklab, var(--vt-bc) 78%, var(--vt-text)); +} +.vt-app .vt-band-2g { --vt-bc: var(--vt-b2); } +.vt-app .vt-band-5g { --vt-bc: var(--vt-b5); } +.vt-app .vt-band-6g { --vt-bc: var(--vt-b6); } + +.vt-app .vt-gen { font-size: 11px; color: var(--vt-text-3); white-space: nowrap; } + +.vt-app .vt-src { + display: inline-flex; align-items: center; height: 16px; padding: 0 5px; + border-radius: 4px; font-size: 9.5px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; + background: var(--vt-surface-3); color: var(--vt-text-3); white-space: nowrap; +} +.vt-app .vt-src-alias { background: var(--vt-accent-soft); color: var(--vt-accent-text); } + +/* avatar: hue from the MAC, icon from the device type */ +.vt-app .vt-av { + display: inline-flex; align-items: center; justify-content: center; flex: none; + width: 34px; height: 34px; border-radius: 10px; + background: hsl(var(--vt-h, 210) var(--vt-av-s) var(--vt-av-l)); + color: hsl(var(--vt-h, 210) 42% var(--vt-av-ink-l)); +} +.vt-app .vt-av .vt-ico { width: 19px; height: 19px; } +.vt-app .vt-av-sm { width: 24px; height: 24px; border-radius: 7px; } +.vt-app .vt-av-sm .vt-ico { width: 14px; height: 14px; } +.vt-app .vt-av-gone { filter: grayscale(1); opacity: .6; } + +/* signal glyph */ +.vt-app .vt-signal { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } +.vt-app .vt-sig { width: 20px; height: 16px; flex: none; } +.vt-app .vt-sig-off { fill: var(--vt-line-2); } +.vt-app .vt-sig.vt-lvl-ok .vt-sig-on { fill: var(--vt-ok); } +.vt-app .vt-sig.vt-lvl-warn .vt-sig-on { fill: var(--vt-warn); } +.vt-app .vt-sig.vt-lvl-crit .vt-sig-on { fill: var(--vt-err); } + +/* compact charts (sparklines): line over a fading area, faint top grid + and baseline, a marker on the newest value, the axis maximum top-right */ +.vt-app .vt-sk { position: relative; --vt-sc: var(--vt-chart-1); } +.vt-app .vt-spark { display: block; width: 100%; height: 34px; overflow: visible; } +.vt-app .vt-sk-empty .vt-spark { opacity: .6; } +.vt-app .vt-spark-grid { fill: none; stroke: var(--vt-chart-grid); stroke-width: 1; stroke-dasharray: 2 3; vector-effect: non-scaling-stroke; } +.vt-app .vt-spark-base { fill: none; stroke: color-mix(in oklab, var(--vt-chart-axis) 35%, transparent); stroke-width: 1; vector-effect: non-scaling-stroke; } +.vt-app .vt-spark-line { fill: none; stroke: var(--vt-sc); stroke-width: 1.5; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; } +.vt-app .vt-spark-area { stroke: none; } +.vt-app .vt-fade-top { stop-color: var(--vt-sc); stop-opacity: var(--vt-fill-op); } +.vt-app .vt-fade-end { stop-color: var(--vt-sc); stop-opacity: 0; } +.vt-app .vt-spark-s1 .vt-spark-line { stroke: var(--vt-chart-axis); stroke-width: 1; stroke-dasharray: 2 2.5; opacity: .75; } +.vt-app .vt-spark-thresh { fill: none; stroke: var(--vt-warn); stroke-width: 1; stroke-dasharray: 3 3; opacity: .75; vector-effect: non-scaling-stroke; } +.vt-app .vt-sk-dot { + position: absolute; width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 50%; + background: var(--vt-sc); box-shadow: 0 0 0 2px var(--vt-surface); pointer-events: none; +} +.vt-app .vt-sk-max { + position: absolute; right: 0; top: -2px; transform: translateY(-100%); + font-size: 10px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none; +} +.vt-app .vt-sk-thresh { + position: absolute; transform: translate(-100%, -120%); + font-size: 10px; line-height: 1; color: var(--vt-warn); white-space: nowrap; pointer-events: none; +} +.vt-app .vt-sk-note { position: absolute; right: 0; bottom: 4px; font-size: 10.5px; color: var(--vt-chart-axis); } +.vt-app .vt-spark-cpu { --vt-sc: var(--vt-chart-5); } +.vt-app .vt-spark-2g { --vt-sc: var(--vt-b2); } +.vt-app .vt-spark-5g { --vt-sc: var(--vt-b5); } +.vt-app .vt-spark-6g { --vt-sc: var(--vt-b6); } +.vt-app .vt-spark-x { --vt-sc: var(--vt-text-3); } +.vt-app .vt-spark-up { --vt-sc: var(--vt-series-up); } +.vt-app .vt-cap { display: block; margin-top: 3px; font-size: 10px; line-height: 1.1; color: var(--vt-chart-axis); letter-spacing: .02em; white-space: nowrap; } +.vt-app .vt-sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } + +/* ------------------------------------------------------------ 3. layout */ + +.vt-app .vt-grid { + display: grid; + gap: 14px; + grid-template-columns: minmax(0, 1fr); + grid-template-areas: "clients" "talk" "radios" "hero" "ssids" "chart" "sys"; +} +.vt-app .vt-hero { grid-area: hero; } +.vt-app .vt-sys { grid-area: sys; } +.vt-app .vt-chart { grid-area: chart; } +.vt-app .vt-radios { grid-area: radios; } +.vt-app .vt-clients { grid-area: clients; } +.vt-app .vt-side { grid-area: side; display: flex; flex-direction: column; gap: 14px; min-width: 0; } +/* phones: the side column dissolves so Top talkers can follow Clients */ +@container vt (max-width: 699px) { + .vt-app .vt-side { display: contents; } + .vt-app .vt-talk { grid-area: talk; } + .vt-app .vt-ssid-tile { grid-area: ssids; } +} + +@container vt (min-width: 700px) { + .vt-app .vt-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + grid-template-areas: "hero hero" "radios radios" "clients clients" "chart chart" "sys side"; + } +} +@container vt (min-width: 1100px) { + .vt-app .vt-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + grid-template-areas: + "hero hero hero sys" + "chart chart radios radios" + "clients clients clients side"; + } +} + +.vt-app .vt-tile { + position: relative; + min-width: 0; + background: var(--vt-surface); + border: 1px solid var(--vt-line); + border-radius: var(--vt-r-lg); + box-shadow: var(--vt-shadow); + padding: 16px 18px 18px; +} +.vt-app .vt-tile-head { + display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; + min-height: 28px; margin-bottom: 12px; +} +.vt-app .vt-tile-title { + display: inline-flex; align-items: center; gap: 8px; min-width: 0; + font-size: 13px; font-weight: 650; letter-spacing: .005em; color: var(--vt-text); + white-space: nowrap; +} +.vt-app .vt-tile-title .vt-ico { color: var(--vt-text-3); width: 17px; height: 17px; } +.vt-app .vt-tile-extra { display: flex; align-items: center; gap: 8px; min-width: 0; } + +/* folding: phones only; wider layouts show everything and ignore the state */ +.vt-app .vt-fold-btn, .vt-app .vt-sec-head, .vt-app .vt-radio-sum { display: none; } +@container vt (max-width: 699px) { + .vt-app .vt-fold-btn { + display: inline-flex; align-items: center; justify-content: center; flex: none; + width: 32px; height: 32px; margin: -4px -8px -4px 0; padding: 0; border: 0; border-radius: 8px; + background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0; + } + .vt-app .vt-fold-btn:hover { background: var(--vt-surface-3); color: var(--vt-text); } + .vt-app .vt-fold-btn .vt-ico { width: 16px; height: 16px; transform: rotate(-90deg); transition: transform .2s; } + .vt-app .vt-fold-btn[aria-expanded="false"] .vt-ico { transform: rotate(90deg); } + .vt-app .vt-folded > :not(.vt-tile-head):not(.vt-fold-keep) { display: none !important; } + .vt-app .vt-folded > .vt-tile-head { margin-bottom: 0; } + .vt-app .vt-folded.vt-hero > .vt-tile-head { margin-bottom: 2px; } + .vt-app .vt-folded.vt-hero .vt-kpis { margin-top: 4px; } + .vt-app .vt-hint-wide { display: none; } + + /* radios become one tile: a head, then condensed rows or flat cards */ + .vt-app .vt-radios { + padding: 14px 14px 10px; background: var(--vt-surface); border: 1px solid var(--vt-line); + border-radius: var(--vt-r-lg); box-shadow: var(--vt-shadow); + } + .vt-app .vt-sec-head { display: flex; } + .vt-app .vt-radios.vt-folded .vt-radio-sum { display: flex; flex-direction: column; margin-top: 8px; } + .vt-app .vt-radio-list { gap: 0; } + .vt-app .vt-radio-list > .vt-radio { border: 0; border-top: 1px solid var(--vt-line); border-radius: 0; box-shadow: none; padding: 14px 0 12px; background: transparent; } + .vt-app .vt-radio-list > .vt-radio::before { display: none; } +} +.vt-app .vt-rsum { + display: flex; align-items: center; gap: 8px; width: 100%; min-height: 44px; padding: 8px 2px; margin: 0; + border: 0; border-top: 1px solid var(--vt-line); border-radius: 0; background: transparent; box-shadow: none; + color: var(--vt-text); text-align: left; cursor: pointer; font-weight: 400; +} +.vt-app .vt-rsum:hover { background: var(--vt-surface-2); } +.vt-app .vt-rsum .vt-status { margin: 0; } +.vt-app .vt-rsum-main { font-weight: 650; font-size: 13px; white-space: nowrap; } +.vt-app .vt-rsum-sub { font-size: 12px; color: var(--vt-text-3); white-space: nowrap; } +.vt-app .vt-rsum-rate { margin-left: auto; font-size: 12px; color: var(--vt-text-2); white-space: nowrap; } +.vt-app .vt-rsum > .vt-ico { color: var(--vt-text-3); } + +.vt-app .vt-empty { + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; + padding: 32px 16px; text-align: center; color: var(--vt-text-3); +} +.vt-app .vt-empty > .vt-ico { width: 28px; height: 28px; stroke-width: 1.4; margin-bottom: 4px; } +.vt-app .vt-empty-title { color: var(--vt-text); font-weight: 620; font-size: 14px; } +.vt-app .vt-empty .vt-btn { margin-top: 8px; } +.vt-app .vt-empty-inline { color: var(--vt-text-3); font-size: 12.5px; padding: 4px 0; } + +/* ----------------------------------------------------- 4. head, health */ + +.vt-app .vt-head { + display: flex; align-items: center; gap: 14px; + margin: 2px 0 14px; +} +.vt-app .vt-orb { + position: relative; flex: none; + width: 38px; height: 38px; border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + --vt-oc: var(--vt-ok); + background: color-mix(in oklab, var(--vt-oc) 16%, transparent); +} +.vt-app .vt-orb::after { + content: ""; position: absolute; inset: 0; border-radius: 50%; + border: 2px solid var(--vt-oc); opacity: .35; + animation: vt-ring 2.6s ease-out infinite; +} +.vt-app .vt-orb-core { width: 14px; height: 14px; border-radius: 50%; background: var(--vt-oc); box-shadow: 0 0 0 4px color-mix(in oklab, var(--vt-oc) 22%, transparent); } +.vt-app .vt-orb-warn { --vt-oc: var(--vt-warn); } +.vt-app .vt-orb-err { --vt-oc: var(--vt-err); } +@keyframes vt-ring { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(1.35); opacity: 0; } } + +.vt-app .vt-head-text { min-width: 0; flex: 1; } +.vt-app .vt-headline { + font-size: 21px; font-weight: 680; letter-spacing: -.015em; line-height: 1.2; color: var(--vt-text); + margin: 0; padding: 0; border: 0; text-transform: none; +} +.vt-app .vt-subline { color: var(--vt-text-3); font-size: 12.5px; margin-top: 2px; } + +.vt-app .vt-livepill { + flex: none; display: inline-flex; align-items: center; gap: 8px; + height: 30px; padding: 0 12px 0 10px; border-radius: 999px; + border: 1px solid var(--vt-line); background: var(--vt-surface); + font-size: 12px; font-weight: 560; color: var(--vt-text-2); white-space: nowrap; +} +.vt-app .vt-live-beat { width: 8px; height: 8px; border-radius: 50%; background: var(--vt-ok); box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 50%, transparent); animation: vt-beat 2s infinite; } +.vt-app .vt-live-paused .vt-live-beat { background: var(--vt-text-3); animation: none; } +.vt-app .vt-live-err .vt-live-beat { background: var(--vt-warn); } +@keyframes vt-beat { 0% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--vt-ok) 45%, transparent); } 70% { box-shadow: 0 0 0 7px transparent; } 100% { box-shadow: 0 0 0 0 transparent; } } + +.vt-app .vt-health { + display: grid; gap: 10px; margin-bottom: 14px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} +@container vt (min-width: 700px) { .vt-app .vt-health { grid-template-columns: repeat(3, minmax(0, 1fr)); } } +@container vt (min-width: 1100px) { .vt-app .vt-health { grid-template-columns: repeat(6, minmax(0, 1fr)); } } +.vt-app .vt-chip { + display: flex; align-items: flex-start; gap: 10px; min-width: 0; + padding: 11px 12px; border-radius: var(--vt-r); + border: 1px solid var(--vt-line); background: var(--vt-surface); + color: var(--vt-text); text-decoration: none; text-align: left; cursor: pointer; + box-shadow: var(--vt-shadow); + transition: border-color .15s, transform .15s, background .15s; + --vt-cc: var(--vt-ok); +} +.vt-app .vt-chip:hover { text-decoration: none; border-color: color-mix(in oklab, var(--vt-cc) 55%, var(--vt-line)); transform: translateY(-1px); } +.vt-app .vt-chip-warn { --vt-cc: var(--vt-warn); background: color-mix(in oklab, var(--vt-warn) 7%, var(--vt-surface)); border-color: color-mix(in oklab, var(--vt-warn) 35%, var(--vt-line)); } +.vt-app .vt-chip-err { --vt-cc: var(--vt-err); background: color-mix(in oklab, var(--vt-err) 7%, var(--vt-surface)); border-color: color-mix(in oklab, var(--vt-err) 35%, var(--vt-line)); } +.vt-app .vt-chip-info { --vt-cc: var(--vt-text-3); } +.vt-app .vt-chip-ico { + flex: none; width: 26px; height: 26px; border-radius: 8px; + display: inline-flex; align-items: center; justify-content: center; + background: color-mix(in oklab, var(--vt-cc) 15%, transparent); color: var(--vt-cc); +} +.vt-app .vt-chip-ico .vt-ico { width: 15px; height: 15px; stroke-width: 2.1; } +.vt-app .vt-chip-main { display: flex; flex-direction: column; min-width: 0; flex: 1; } +.vt-app .vt-chip-top { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; } +.vt-app .vt-chip-title { font-size: 11px; line-height: 1.3; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; } +.vt-app .vt-chip-value { font-size: 15px; font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; } +.vt-app .vt-chip-reason { + font-size: 12px; color: var(--vt-text-2); margin-top: 2px; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; +} + +/* -------------------------------------------------- 5. hero: network path */ + +.vt-app .vt-hero { container: vthero / inline-size; overflow: hidden; display: flex; flex-direction: column; } + +.vt-app .vt-kpis { + position: relative; z-index: 1; + display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; + margin-top: 10px; padding-top: 14px; border-top: 1px solid var(--vt-line); +} +.vt-app .vt-kpi { + display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 0; + padding: 6px 10px; margin: 0; border: 0; border-radius: var(--vt-r-sm); + background: transparent; color: var(--vt-text); text-align: left; box-shadow: none; height: auto; text-transform: none; font-weight: 400; +} +.vt-app button.vt-kpi { cursor: pointer; } +.vt-app button.vt-kpi:hover { background: var(--vt-surface-2); } +.vt-app .vt-kpi + .vt-kpi { box-shadow: -1px 0 0 var(--vt-line); border-radius: 0 var(--vt-r-sm) var(--vt-r-sm) 0; } +.vt-app .vt-kpi-label { font-size: 11px; font-weight: 650; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; } +.vt-app .vt-kpi-value { font-size: 22px; font-weight: 700; letter-spacing: -.02em; line-height: 1.2; white-space: nowrap; } +.vt-app .vt-kpi-sub { font-size: 12px; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-kpi-warn .vt-kpi-value { color: var(--vt-warn); } +@container vthero (max-width: 700px) { + .vt-app .vt-kpis { grid-template-columns: 1fr 1fr; } + .vt-app .vt-kpi:nth-child(3) { box-shadow: none; } +} +.vt-app .vt-hero::before { + content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; + background: + radial-gradient(circle at 1px 1px, color-mix(in oklab, var(--vt-text-3) 22%, transparent) 1px, transparent 1.4px) 0 0 / 18px 18px; + -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.55), transparent 85%); + mask-image: linear-gradient(180deg, rgba(0,0,0,.55), transparent 85%); +} +.vt-app .vt-path { + position: relative; + display: grid; + grid-template-columns: minmax(150px, 1.05fr) minmax(96px, .7fr) minmax(184px, 1.3fr) minmax(0, 2.6fr); + align-items: stretch; + flex: 1; + min-height: 210px; + padding: 4px 0 8px; +} +.vt-app .vt-wires { position: absolute; left: 0; top: 0; z-index: 0; pointer-events: none; overflow: visible; } +.vt-app .vt-wire { fill: none; stroke: var(--vt-line-2); stroke-width: 1.5; transition: stroke .4s; } +.vt-app .vt-wire-main { stroke-width: 2.5; } +.vt-app .vt-wire-on { stroke: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line-2)); } +.vt-app .vt-flow { + fill: none; stroke: var(--vt-accent-line); stroke-width: 2.4; stroke-linecap: round; + stroke-dasharray: 1.5 14.5; opacity: 0; transition: opacity .4s; +} +.vt-app .vt-flow-on { opacity: .95; animation: vt-flow 1.6s linear infinite; } +.vt-app .vt-flow-2.vt-flow-on { animation-duration: 1.05s; } +.vt-app .vt-flow-3.vt-flow-on { animation-duration: .6s; stroke-width: 3; } +@keyframes vt-flow { to { stroke-dashoffset: -32; } } + +.vt-app .vt-node { + position: relative; z-index: 1; + display: flex; align-items: center; gap: 10px; min-width: 0; + padding: 9px 11px; margin: 0; + border: 1px solid var(--vt-line); border-radius: var(--vt-r); + background: var(--vt-surface); color: var(--vt-text); + box-shadow: var(--vt-shadow); + text-align: left; cursor: pointer; height: auto; line-height: 1.3; text-transform: none; font-weight: 400; + transition: border-color .15s, transform .15s, box-shadow .15s; +} +.vt-app .vt-node:hover { border-color: color-mix(in oklab, var(--vt-accent-line) 60%, var(--vt-line)); transform: translateY(-1px); box-shadow: 0 6px 16px rgba(12, 14, 17, .08); background: var(--vt-surface); } +.vt-app .vt-node-body { display: flex; flex-direction: column; min-width: 0; gap: 1px; } +.vt-app .vt-node-kicker { display: flex; align-items: center; gap: 6px; font-size: 11px; font-weight: 600; color: var(--vt-text-3); text-transform: uppercase; letter-spacing: .05em; white-space: nowrap; } +.vt-app .vt-node-title { font-size: 13.5px; font-weight: 650; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-node-sub { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-node-ico { + flex: none; width: 32px; height: 32px; border-radius: 9px; + display: inline-flex; align-items: center; justify-content: center; + background: var(--vt-surface-3); color: var(--vt-text-2); +} +.vt-app .vt-node-ico .vt-ico { width: 20px; height: 20px; } +.vt-app .vt-node-gw { justify-self: stretch; align-self: center; } +.vt-app .vt-node-ap { justify-self: stretch; align-self: center; padding: 12px 13px; border-color: color-mix(in oklab, var(--vt-accent-line) 40%, var(--vt-line)); } +.vt-app .vt-node-ap .vt-node-ico { background: var(--vt-accent); color: var(--vt-accent-ink); } +.vt-app .vt-node-ap .vt-node-title { font-size: 14.5px; } +.vt-app .vt-node-gen { text-transform: none; letter-spacing: 0; font-weight: 560; } +.vt-app .vt-node-radio { padding: 8px 11px; } +.vt-app .vt-node-ssid { padding: 7px 10px; } +.vt-app .vt-node-ssid .vt-node-title { display: flex; align-items: center; gap: 6px; font-size: 13px; } +.vt-app .vt-node-ssid .vt-node-title .vt-ico { color: var(--vt-text-3); } +.vt-app .vt-ssid-name span { overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-node-idle { background: var(--vt-surface-2); box-shadow: none; } +.vt-app .vt-node-idle .vt-node-title { color: var(--vt-text-2); font-weight: 600; } +.vt-app .vt-node-off { opacity: .6; border-style: dashed; } + +.vt-app .vt-dots { display: inline-flex; gap: 3px; } +.vt-app .vt-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--vt-text-3); } +.vt-app .vt-dot.vt-lvl-ok { background: var(--vt-ok); } +.vt-app .vt-dot.vt-lvl-warn { background: var(--vt-warn); } +.vt-app .vt-dot.vt-lvl-crit { background: var(--vt-err); } + +.vt-app .vt-link-label { + position: relative; z-index: 1; + display: grid; grid-template-rows: minmax(0, 1fr) 14px minmax(0, 1fr); justify-items: center; + align-self: stretch; padding: 0 6px; text-align: center; +} +.vt-app .vt-link-below { grid-row: 3; align-self: start; display: flex; flex-direction: column; align-items: center; } +.vt-app .vt-link-rate { + display: inline-flex; align-items: baseline; gap: 3px; white-space: nowrap; + padding: 0 6px; border-radius: 6px; background: var(--vt-surface); +} +.vt-app .vt-link-rate .vt-ico { width: 13px; height: 13px; align-self: center; stroke-width: 2.2; } +.vt-app .vt-link-rate b { font-size: 17px; font-weight: 700; letter-spacing: -.01em; } +.vt-app .vt-link-rate small { font-size: 11px; color: var(--vt-text-3); font-weight: 600; } +.vt-app .vt-link-down { grid-row: 1; align-self: end; } +.vt-app .vt-link-down .vt-ico { color: var(--vt-accent-text); } +.vt-app .vt-link-up { font-size: 12px; } +.vt-app .vt-link-up b { font-size: 13px; font-weight: 650; color: var(--vt-text-2); } +.vt-app .vt-link-up .vt-ico { color: var(--vt-text-3); } +.vt-app .vt-link-meta { font-size: 11px; color: var(--vt-text-3); font-family: var(--vt-mono); white-space: nowrap; margin-top: 2px; } + +/* the branches spread over the height the tile gets from its row, so a + tall neighbour leaves no empty band above or below the diagram */ +.vt-app .vt-branches { display: flex; flex-direction: column; justify-content: space-between; gap: 18px; padding: 2px 0 2px 34px; min-width: 0; } +.vt-app .vt-branch { display: grid; grid-template-columns: minmax(136px, 172px) minmax(0, 1fr); column-gap: 34px; align-items: center; min-width: 0; } +.vt-app .vt-branch-ssids { display: flex; flex-direction: column; gap: 7px; min-width: 0; } +.vt-app .vt-branch-ssids .vt-node { max-width: 260px; } + +@container vthero (max-width: 700px) { + .vt-app .vt-path { grid-template-columns: minmax(0, 1fr); gap: 12px; min-height: 0; } + .vt-app .vt-node-gw, .vt-app .vt-node-ap { justify-self: start; min-width: 220px; } + .vt-app .vt-link-label { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 10px; justify-content: flex-start; padding: 0 0 0 44px; } + .vt-app .vt-link-label > * { align-self: center; } + .vt-app .vt-branches { padding-left: 34px; gap: 12px; } + .vt-app .vt-branch { grid-template-columns: minmax(0, 1fr); row-gap: 8px; } + .vt-app .vt-branch > .vt-node { justify-self: start; min-width: 200px; } + .vt-app .vt-branch-ssids { padding-left: 34px; } + .vt-app .vt-branch-ssids .vt-node { max-width: none; align-self: stretch; } +} + +/* ------------------------------------------------------------ 6. system */ + +.vt-app .vt-sys { display: flex; flex-direction: column; gap: 16px; } +.vt-app .vt-sys .vt-tile-head { margin-bottom: 0; } +.vt-app .vt-sys-cpu { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 16px; align-items: end; } +.vt-app .vt-sys-cpu-l { display: flex; flex-direction: column; gap: 3px; } +.vt-app .vt-sys-cpu-r { display: flex; align-items: flex-end; gap: 10px; min-width: 0; } +.vt-app .vt-spark-box { flex: 1; min-width: 0; padding-top: 12px; } +.vt-app .vt-spark-box .vt-spark { height: 38px; } +.vt-app .vt-cores-box { display: flex; flex-direction: column; align-items: center; flex: none; cursor: help; } +.vt-app .vt-cores { display: flex; gap: 3px; height: 38px; align-items: flex-end; } +.vt-app .vt-core { position: relative; width: 6px; height: 100%; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; } +.vt-app .vt-core-fill { position: absolute; left: 0; right: 0; bottom: 0; border-radius: 3px; background: var(--vt-chart-5); transition: height .6s ease; } + +.vt-app .vt-sys-mem { display: flex; flex-direction: column; gap: 7px; } +.vt-app .vt-mem-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; flex-wrap: wrap; } +.vt-app .vt-mem-avail { font-size: 17px; font-weight: 680; } +.vt-app .vt-mem-of { font-size: 12px; color: var(--vt-text-3); } +.vt-app .vt-seg { display: flex; height: 12px; border-radius: 6px; overflow: hidden; background: var(--vt-surface-3); gap: 2px; } +.vt-app .vt-seg-part { height: 100%; min-width: 0; transition: width .7s cubic-bezier(.2, .7, .2, 1); } +.vt-app .vt-seg-used { background: var(--vt-mem); border-radius: 6px 2px 2px 6px; } +.vt-app .vt-seg-cache { + background: + repeating-linear-gradient(135deg, color-mix(in oklab, var(--vt-mem) 42%, transparent) 0 3px, color-mix(in oklab, var(--vt-mem) 24%, transparent) 3px 6px); + border-radius: 2px; +} +.vt-app .vt-seg-free { background: transparent; } +.vt-app .vt-legend { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 12px; color: var(--vt-text-2); } +.vt-app .vt-legend-item { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } +.vt-app .vt-legend-item b { font-weight: 650; color: var(--vt-text); } +.vt-app .vt-key { width: 10px; height: 10px; border-radius: 3px; flex: none; } +.vt-app .vt-key.vt-seg-free { background: var(--vt-surface-3); box-shadow: inset 0 0 0 1px var(--vt-line-2); } +.vt-app .vt-swap { display: flex; align-items: center; gap: 8px; font-size: 12px; } +.vt-app .vt-swap:empty { display: none; } +.vt-app .vt-thin { flex: 1; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; } +.vt-app .vt-thin-fill { display: block; height: 100%; background: var(--vt-mem); } + +.vt-app .vt-sys-store { display: flex; flex-direction: column; gap: 8px; } +.vt-app .vt-storage { display: flex; flex-direction: column; gap: 8px; } +.vt-app .vt-mount-top { display: flex; justify-content: space-between; gap: 8px; font-size: 12.5px; margin-bottom: 4px; } +.vt-app .vt-bar { height: 6px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; } +.vt-app .vt-bar-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-mem); transition: width .7s ease; } +.vt-app .vt-bg-warn { background: var(--vt-warn); } +.vt-app .vt-bg-err { background: var(--vt-err); } +.vt-app .vt-fw { display: flex; flex-direction: column; gap: 1px; margin-top: auto; padding-top: 10px; border-top: 1px dashed var(--vt-line); font-size: 12px; color: var(--vt-text-2); } + +/* ------------------------------------------------------------- 7. chart */ + +/* small multiples: one row per link, each with its own labelled scale, + a shared time axis and one crosshair across all rows */ +.vt-app .vt-chart { display: flex; flex-direction: column; --vt-mh: 136px; --vt-mg: 16px; } +.vt-app .vt-chart .vt-tile-title { flex-wrap: wrap; row-gap: 0; } +.vt-app .vt-chart-sub { font-size: 12px; font-weight: 500; color: var(--vt-text-3); } +.vt-app .vt-chart-sub::before { content: "·"; margin: 0 6px 0 0; } +.vt-app .vt-seg-ctl { display: inline-flex; padding: 2px; gap: 2px; border-radius: 8px; background: var(--vt-surface-3); } +.vt-app .vt-seg-btn { + display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 10px; + border: 0; border-radius: 6px; background: transparent; color: var(--vt-text-2); + font-size: 12px; font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none; +} +.vt-app .vt-seg-btn .vt-ico { width: 13px; height: 13px; stroke-width: 2.2; } +.vt-app .vt-seg-btn:hover { color: var(--vt-text); background: transparent; } +.vt-app .vt-seg-btn[aria-pressed="true"] { background: var(--vt-surface); color: var(--vt-text); box-shadow: 0 1px 2px rgba(0, 0, 0, .12); } + +.vt-app .vt-c-up { --vt-sc: var(--vt-series-up); } +.vt-app .vt-c-2g { --vt-sc: var(--vt-b2); } +.vt-app .vt-c-5g { --vt-sc: var(--vt-b5); } +.vt-app .vt-c-6g { --vt-sc: var(--vt-b6); } +.vt-app .vt-c-x { --vt-sc: var(--vt-text-3); } +.vt-app .vt-lg-key { width: 12px; height: 3px; border-radius: 2px; background: var(--vt-sc); flex: none; } + +.vt-app .vt-mult-wrap { position: relative; flex: 1; display: flex; flex-direction: column; min-height: 240px; border-radius: var(--vt-r-sm); outline-offset: 4px; } +.vt-app .vt-mults { flex: 1; display: grid; grid-auto-rows: minmax(64px, 1fr); } +.vt-app .vt-mult { + display: grid; grid-template-columns: var(--vt-mh) minmax(0, 1fr); column-gap: var(--vt-mg); + padding: 8px 0; border-top: 1px solid var(--vt-line); min-height: 0; +} +.vt-app .vt-mult:first-child { border-top: 0; padding-top: 2px; } +.vt-app .vt-mult-head { display: flex; flex-direction: column; justify-content: center; gap: 1px; min-width: 0; } +.vt-app .vt-mult-name { display: flex; align-items: center; gap: 6px; font-size: 11.5px; font-weight: 600; color: var(--vt-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-mult-now { font-size: 17px; font-weight: 700; letter-spacing: -.01em; line-height: 1.25; white-space: nowrap; } +.vt-app .vt-mult-stats { display: flex; flex-wrap: wrap; gap: 0 8px; font-size: 11px; color: var(--vt-text-3); white-space: nowrap; } +.vt-app .vt-mult-stats b { font-weight: 600; color: var(--vt-text-2); } +.vt-app .vt-mult-plot { position: relative; min-height: 48px; margin-top: 12px; border-bottom: 1px solid color-mix(in oklab, var(--vt-chart-axis) 40%, transparent); --vt-sc: inherit; } +.vt-app .vt-mult-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; } +.vt-app .vt-chart-grid { fill: none; stroke: var(--vt-chart-grid); stroke-width: 1; vector-effect: non-scaling-stroke; } +.vt-app .vt-chart-vgrid { fill: none; stroke: var(--vt-chart-grid); stroke-width: 1; stroke-dasharray: 2 4; vector-effect: non-scaling-stroke; } +.vt-app .vt-series-line { fill: none; stroke: var(--vt-sc); stroke-width: 1.75; vector-effect: non-scaling-stroke; stroke-linejoin: round; stroke-linecap: round; } +.vt-app .vt-series-area { stroke: none; } +.vt-app .vt-mult-max { + position: absolute; right: 0; top: -3px; transform: translateY(-100%); + font-size: 10.5px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; pointer-events: none; +} +.vt-app .vt-mult-dot { + position: absolute; width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px; border-radius: 50%; z-index: 2; + background: var(--vt-sc); box-shadow: 0 0 0 2px var(--vt-surface); pointer-events: none; +} +.vt-app .vt-mult-over { position: absolute; top: 0; bottom: 20px; left: calc(var(--vt-mh) + var(--vt-mg)); right: 0; pointer-events: none; z-index: 3; } +.vt-app .vt-mult-x { position: relative; height: 20px; margin-left: calc(var(--vt-mh) + var(--vt-mg)); } +.vt-app .vt-mx { position: absolute; top: 5px; font-size: 10.5px; line-height: 1; color: var(--vt-chart-axis); white-space: nowrap; transform: translateX(-50%); } +.vt-app .vt-mx-0 { left: 0; transform: none; } +.vt-app .vt-mx-1 { right: 0; transform: none; } +.vt-app .vt-mult-wrap { cursor: crosshair; touch-action: pan-y; } +.vt-app .vt-cursor { position: absolute; top: 0; bottom: 0; width: 0; border-left: 1px solid color-mix(in oklab, var(--vt-text) 45%, transparent); pointer-events: none; } +.vt-app .vt-tip { + position: absolute; top: 4px; z-index: 4; transform: translateX(12px); + min-width: 180px; padding: 8px 10px 7px; border-radius: var(--vt-r); + background: var(--vt-surface); border: 1px solid var(--vt-line-2); + box-shadow: 0 10px 28px rgba(12, 14, 17, .16), 0 2px 6px rgba(12, 14, 17, .06); + font-size: 12px; pointer-events: none; +} +.vt-app .vt-tip-left { transform: translateX(calc(-100% - 12px)); } +.vt-app .vt-tip-time { display: flex; justify-content: space-between; gap: 12px; color: var(--vt-text-3); font-size: 11px; margin-bottom: 5px; padding-bottom: 5px; border-bottom: 1px solid var(--vt-line); } +.vt-app .vt-tip-row { display: grid; grid-template-columns: 12px auto minmax(0, 1fr); align-items: center; column-gap: 8px; line-height: 1.6; } +.vt-app .vt-tip-row b { font-weight: 700; color: var(--vt-text); white-space: nowrap; } +.vt-app .vt-tip-label { color: var(--vt-text-3); white-space: nowrap; text-align: right; } + +/* ------------------------------------------------------------ 8. radios */ + +.vt-app .vt-radios { display: flex; flex-direction: column; min-width: 0; } +.vt-app .vt-radio-list { flex: 1; display: grid; gap: 14px; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); min-width: 0; } +.vt-app .vt-radio { display: flex; flex-direction: column; gap: 12px; --vt-bc: var(--vt-b60); overflow: hidden; } +.vt-app .vt-radio-2g { --vt-bc: var(--vt-b2); } +.vt-app .vt-radio-5g { --vt-bc: var(--vt-b5); } +.vt-app .vt-radio-6g { --vt-bc: var(--vt-b6); } +.vt-app .vt-radio::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--vt-bc); opacity: .85; } +.vt-app .vt-radio-off { opacity: .75; } +.vt-app .vt-radio-head { display: flex; align-items: center; gap: 8px; } +.vt-app .vt-radio-name { font-weight: 650; font-size: 13px; } +.vt-app .vt-status { margin-left: auto; display: inline-flex; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: var(--vt-text-2); } +.vt-app .vt-status::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--vt-ok); } +.vt-app .vt-status-warn::before { background: var(--vt-warn); } +.vt-app .vt-status-err::before { background: var(--vt-err); } +.vt-app .vt-icon-action { + display: inline-flex; align-items: center; justify-content: center; flex: none; + width: 26px; height: 26px; padding: 0; margin: 0; border: 0; border-radius: 7px; + background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0; +} +.vt-app .vt-icon-action:hover { background: var(--vt-surface-3); color: var(--vt-text); } +.vt-app .vt-radio-chan { display: flex; align-items: baseline; gap: 8px; } +.vt-app .vt-radio-width { font-size: 14px; font-weight: 600; color: var(--vt-text-2); } +.vt-app .vt-spectrum { position: relative; height: 8px; border-radius: 4px; background: var(--vt-surface-3); margin-bottom: 12px; } +.vt-app .vt-spectrum-blk { position: absolute; top: -2px; bottom: -2px; border-radius: 3px; background: var(--vt-bc); box-shadow: 0 0 0 2px var(--vt-surface); } +.vt-app .vt-spectrum-lo, .vt-app .vt-spectrum-hi { position: absolute; top: 11px; font-size: 10px; color: var(--vt-text-3); } +.vt-app .vt-spectrum-lo { left: 0; } +.vt-app .vt-spectrum-hi { right: 0; } +.vt-app .vt-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 12px; } +.vt-app .vt-kv dt { font-size: 11px; color: var(--vt-text-3); font-weight: 600; text-transform: uppercase; letter-spacing: .04em; } +.vt-app .vt-kv dd { font-size: 13.5px; font-weight: 600; margin: 1px 0 0; } +.vt-app .vt-kv dd .vt-muted { font-weight: 500; font-size: 12.5px; } +.vt-app .vt-radio-live { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 10px; align-items: center; padding: 8px 10px; border-radius: var(--vt-r-sm); background: var(--vt-surface-2); } +.vt-app .vt-radio-live .vt-spark { height: 30px; } +.vt-app .vt-rates { display: flex; flex-direction: column; gap: 1px; } +.vt-app .vt-rate { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; font-weight: 650; white-space: nowrap; } +.vt-app .vt-rate .vt-ico { width: 12px; height: 12px; stroke-width: 2.3; color: var(--vt-text-3); } +.vt-app .vt-rate-up { font-weight: 500; color: var(--vt-text-2); } +.vt-app .vt-notes { display: flex; flex-direction: column; gap: 4px; } +.vt-app .vt-note { display: flex; align-items: flex-start; gap: 6px; font-size: 12px; color: var(--vt-text-2); } +.vt-app .vt-note .vt-ico { margin-top: 2px; color: var(--vt-text-3); } +.vt-app .vt-note-warn .vt-ico { color: var(--vt-warn); } +.vt-app .vt-note-err .vt-ico { color: var(--vt-err); } +.vt-app .vt-note-info .vt-ico { color: var(--vt-info); } +.vt-app .vt-note-muted { color: var(--vt-text-3); } +.vt-app .vt-radio-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: auto; padding-top: 10px; border-top: 1px solid var(--vt-line); } +.vt-app .vt-avs { display: inline-flex; } +.vt-app .vt-avs .vt-av { box-shadow: 0 0 0 2px var(--vt-surface); } +.vt-app .vt-avs .vt-av + .vt-av { margin-left: -6px; } + +/* ----------------------------------------------------------- 9. clients */ + +.vt-app .vt-clients { padding-bottom: 8px; } +.vt-app .vt-count { font-size: 12px; color: var(--vt-text-3); font-weight: 560; } +.vt-app .vt-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 10px; } +.vt-app .vt-filter { + position: relative; display: flex; align-items: center; flex: 1 1 260px; max-width: 380px; + height: 34px; padding: 0 8px 0 10px; gap: 6px; + border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); background: var(--vt-surface); + color: var(--vt-text-3); cursor: text; +} +.vt-app .vt-filter:focus-within { border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); } +.vt-app .vt-filter .vt-ico { width: 16px; height: 16px; } +.vt-app .vt-filter .vt-filter-input { + flex: 1; min-width: 0; width: auto; height: 32px; padding: 0; margin: 0; + border: 0; background: transparent; box-shadow: none; outline: none; + color: var(--vt-text); font: inherit; font-size: 13px; +} +.vt-app .vt-filter .vt-filter-input::placeholder { color: var(--vt-text-3); opacity: 1; } +.vt-app .vt-key-hint { font: 600 10.5px/1 var(--vt-mono); padding: 3px 5px; border-radius: 4px; border: 1px solid var(--vt-line-2); color: var(--vt-text-3); background: var(--vt-surface-2); } +.vt-app .vt-filter:focus-within .vt-key-hint { display: none; } +.vt-app .vt-filter-clear { + display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; padding: 0; margin: 0; + border: 0; border-radius: 5px; background: transparent; color: var(--vt-text-3); cursor: pointer; box-shadow: none; min-width: 0; +} +.vt-app .vt-filter-clear:hover { background: var(--vt-surface-3); color: var(--vt-text); } +.vt-app .vt-filter-clear .vt-ico { width: 14px; height: 14px; } +.vt-app .vt-fchips { display: flex; flex-wrap: wrap; gap: 6px; } +.vt-app .vt-fchip { + display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; margin: 0; + border: 1px solid var(--vt-line); border-radius: 999px; background: var(--vt-surface); + color: var(--vt-text-2); font-size: 12px; font-weight: 600; cursor: pointer; box-shadow: none; min-width: 0; text-transform: none; +} +.vt-app .vt-fchip:hover { border-color: var(--vt-line-2); color: var(--vt-text); background: var(--vt-surface-2); } +.vt-app .vt-fchip-n { font-size: 11px; color: var(--vt-text-3); font-weight: 600; } +.vt-app .vt-fchip-warn .vt-fchip-n { color: var(--vt-warn); } +.vt-app .vt-fchip.vt-fchip-on { background: var(--vt-text); border-color: var(--vt-text); color: var(--vt-surface); } +.vt-app .vt-fchip.vt-fchip-on .vt-fchip-n { color: color-mix(in oklab, var(--vt-surface) 70%, transparent); } + +.vt-app .vt-table-wrap { overflow-x: auto; margin: 0 -18px; padding: 0 18px; } +.vt-app .vt-ctable { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: auto; min-width: 800px; } +.vt-app .vt-ctable th { + position: sticky; top: 0; z-index: 1; + padding: 0 10px; height: 34px; text-align: left; white-space: nowrap; letter-spacing: normal; text-transform: none; + background: var(--vt-surface); border-bottom: 1px solid var(--vt-line); + font-weight: 400; +} +.vt-app .vt-sort { + display: inline-flex; align-items: center; gap: 4px; height: 26px; padding: 0 4px; margin: 0 -4px; + border: 0; border-radius: 5px; background: transparent; box-shadow: none; min-width: 0; + color: var(--vt-text-3); font-size: 11px; font-weight: 650; letter-spacing: .05em; text-transform: uppercase; cursor: pointer; +} +.vt-app .vt-sort:hover { color: var(--vt-text); background: var(--vt-surface-2); } +.vt-app .vt-sort-on { color: var(--vt-text); } +.vt-app .vt-sort-ind { font-size: 11px; width: 8px; } +.vt-app .vt-ctable td { + height: auto; line-height: 1.4; overflow-wrap: normal; text-align: left; + padding: 9px 8px; vertical-align: middle; + border-bottom: 1px solid var(--vt-line); +} +.vt-app .vt-ctable tr:last-child td { border-bottom: 0; } +.vt-app .vt-row { cursor: pointer; transition: background .15s; } +.vt-app .vt-row:hover td { background: var(--vt-surface-2); } +.vt-app .vt-row:focus-visible { outline: none; } +.vt-app .vt-row:focus-visible td { background: var(--vt-accent-soft); } +.vt-app .vt-row-sel td { background: var(--vt-accent-soft); } +.vt-app .vt-row-sel td:first-child { box-shadow: inset 3px 0 0 var(--vt-accent-line); } +.vt-app .vt-row-enter { animation: vt-in .45s ease both; } +.vt-app .vt-row-gone td { color: var(--vt-text-3); } +.vt-app .vt-row-gone .vt-who-label { color: var(--vt-text-3); text-decoration: line-through; text-decoration-color: var(--vt-line-2); } +@keyframes vt-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } } + +.vt-app .vt-who { display: flex; align-items: center; gap: 10px; min-width: 0; } +.vt-app .vt-who-text { min-width: 0; } +/* names get two lines before they are cut; the full name is the title */ +.vt-app .vt-who-label { + font-weight: 640; font-size: 13.5px; line-height: 1.3; max-width: 280px; overflow-wrap: anywhere; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; +} +.vt-app .vt-who-sub { display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin-top: 3px; color: var(--vt-text-3); font-size: 11.5px; } +.vt-app .vt-who-id { white-space: nowrap; } +.vt-app .vt-td-name { min-width: 250px; } +.vt-app .vt-net { display: flex; flex-direction: column; gap: 3px; } +.vt-app .vt-net-ssid { font-weight: 560; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 150px; } +.vt-app .vt-net-badges { display: flex; align-items: center; gap: 6px; } +.vt-app .vt-phy { display: flex; align-items: center; gap: 4px; font-size: 12.5px; white-space: nowrap; } +.vt-app .vt-phy b { font-weight: 620; } +.vt-app .vt-phy .vt-ico { color: var(--vt-text-3); stroke-width: 2.2; width: 12px; height: 12px; } +.vt-app .vt-phy-up { color: var(--vt-text-3); font-size: 11.5px; padding-left: 16px; } +.vt-app .vt-live-cell { display: grid; grid-template-columns: auto; gap: 3px; min-width: 96px; } +.vt-app .vt-live-total { font-weight: 650; } +.vt-app .vt-mini { display: block; height: 4px; border-radius: 2px; background: var(--vt-surface-3); overflow: hidden; } +.vt-app .vt-mini-fill { display: block; height: 100%; background: var(--vt-info); border-radius: 2px; transition: width .6s ease; } +.vt-app .vt-live-split { font-size: 11px; color: var(--vt-text-3); white-space: nowrap; } +.vt-app .vt-exp { display: flex; align-items: center; gap: 9px; min-width: 0; } +.vt-app .vt-score { + flex: none; display: inline-flex; align-items: center; justify-content: center; + width: 32px; height: 26px; border-radius: 7px; font-size: 12.5px; font-weight: 700; + --vt-xc: var(--vt-ok); background: color-mix(in oklab, var(--vt-xc) 15%, transparent); color: color-mix(in oklab, var(--vt-xc) 85%, var(--vt-text)); +} +.vt-app .vt-score-warn { --vt-xc: var(--vt-warn); } +.vt-app .vt-score-err { --vt-xc: var(--vt-err); } +.vt-app .vt-exp-text { display: flex; flex-direction: column; min-width: 0; } +.vt-app .vt-exp-text b { font-size: 12.5px; font-weight: 650; } +.vt-app .vt-exp-text span { + font-size: 11.5px; line-height: 1.35; color: var(--vt-text-3); max-width: 170px; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; +} +.vt-app .vt-td-time { white-space: nowrap; color: var(--vt-text-2); } +/* the Graphite shell tags numeric-looking cells with .vt-num (monospace); + the dashboard sets its own type */ +.vt-app td.vt-num { font-family: inherit; font-size: inherit; letter-spacing: inherit; } + +/* ---------------------------------------------------- 10. talkers, SSIDs */ + +.vt-app .vt-talkers { display: flex; flex-direction: column; gap: 2px; } +.vt-app .vt-talk-row, .vt-app .vt-ssid, .vt-app .vt-dlist-row { + display: flex; align-items: center; gap: 10px; width: 100%; + padding: 8px; + border: 0; border-radius: var(--vt-r-sm); background: transparent; box-shadow: none; + text-align: left; cursor: pointer; color: var(--vt-text); height: auto; min-width: 0; text-transform: none; font-weight: 400; +} +.vt-app .vt-talk-row:hover, .vt-app .vt-ssid:hover, .vt-app .vt-dlist-row:hover { background: var(--vt-surface-2); } +.vt-app .vt-talk-rank { width: 14px; font-size: 12px; color: var(--vt-text-3); font-weight: 650; text-align: center; flex: none; } +.vt-app .vt-talk-main { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } +.vt-app .vt-talk-top { display: flex; justify-content: space-between; gap: 10px; } +.vt-app .vt-talk-name { font-weight: 600; line-height: 1.3; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.vt-app .vt-talk-share { font-size: 12px; font-weight: 650; color: var(--vt-text-2); white-space: nowrap; } +.vt-app .vt-talk-sub b { color: var(--vt-text); font-weight: 650; } +.vt-app .vt-talk-bar { height: 5px; border-radius: 3px; background: var(--vt-surface-3); overflow: hidden; } +.vt-app .vt-talk-fill { display: block; height: 100%; border-radius: 3px; background: var(--vt-info); transition: width .6s ease; } +.vt-app .vt-talk-sub { display: flex; gap: 8px; align-items: baseline; font-size: 11px; color: var(--vt-text-3); white-space: nowrap; overflow: hidden; } + +.vt-app .vt-ssids { display: flex; flex-direction: column; gap: 2px; } +.vt-app .vt-ssid-ico { flex: none; width: 30px; height: 30px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); } +.vt-app .vt-ssid-ico .vt-ico { width: 16px; height: 16px; } +.vt-app .vt-ssid-ico.vt-t-warn { background: var(--vt-warn-soft); color: var(--vt-warn); } +.vt-app .vt-ssid-main { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; } +.vt-app .vt-ssid-top { display: flex; align-items: center; gap: 6px; min-width: 0; } +.vt-app .vt-ssid-name { font-weight: 640; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.vt-app .vt-ssid-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; } +.vt-app .vt-sec { font-size: 11px; font-weight: 600; color: var(--vt-text-2); } +.vt-app .vt-ssid-side { display: flex; flex-direction: column; align-items: flex-end; flex: none; } +.vt-app .vt-ssid-n { font-size: 17px; font-weight: 700; line-height: 1.1; } +.vt-app .vt-ssid-idle .vt-ssid-n { color: var(--vt-text-3); } + +/* ------------------------------------------------------------ 11. drawer */ + +.vt-app .vt-drawer-back { + position: fixed; inset: 0; z-index: 880; + background: var(--vt-scrim); opacity: 0; transition: opacity .25s ease; +} +.vt-app .vt-drawer { + position: fixed; z-index: 890; top: 0; right: 0; bottom: 0; + width: min(460px, 100vw); + display: flex; flex-direction: column; + background: var(--vt-surface); color: var(--vt-text); + border-left: 1px solid var(--vt-line); + box-shadow: var(--vt-sheet-shadow); + transform: translateX(104%); + transition: transform .28s cubic-bezier(.2, .8, .2, 1); + outline: none; +} +.vt-app.vt-drawer-open .vt-drawer { transform: none; } +.vt-app.vt-drawer-open .vt-drawer-back { opacity: 1; } +.vt-app .vt-drawer-grip { display: none; } +.vt-app .vt-drawer-head { + display: flex; align-items: center; gap: 12px; + padding: 16px 16px 14px 20px; border-bottom: 1px solid var(--vt-line); +} +.vt-app .vt-drawer-title { display: flex; align-items: center; gap: 12px; flex: 1; min-width: 0; } +.vt-app .vt-drawer-title .vt-av { width: 44px; height: 44px; border-radius: 12px; } +.vt-app .vt-drawer-title .vt-av .vt-ico { width: 24px; height: 24px; } +.vt-app .vt-drawer-ico { width: 44px; height: 44px; border-radius: 12px; display: inline-flex; align-items: center; justify-content: center; background: var(--vt-surface-3); color: var(--vt-text-2); flex: none; } +.vt-app .vt-drawer-ico .vt-ico { width: 22px; height: 22px; } +.vt-app .vt-drawer-titles { min-width: 0; } +.vt-app .vt-drawer-name { font-size: 17px; font-weight: 680; letter-spacing: -.01em; line-height: 1.25; overflow-wrap: anywhere; } +.vt-app .vt-drawer-sub { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; color: var(--vt-text-3); font-size: 12px; margin-top: 3px; } +.vt-app .vt-drawer-close { + display: inline-flex; align-items: center; justify-content: center; flex: none; + width: 34px; height: 34px; padding: 0; margin: 0; border: 0; border-radius: 9px; + background: transparent; color: var(--vt-text-2); cursor: pointer; box-shadow: none; min-width: 0; +} +.vt-app .vt-drawer-close:hover { background: var(--vt-surface-3); color: var(--vt-text); } +.vt-app .vt-drawer-body { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 14px 20px 28px; display: flex; flex-direction: column; gap: 16px; } +.vt-app .vt-dactions { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; } +.vt-app .vt-dactions:has(> .vt-link:only-child) { justify-content: flex-end; } +.vt-app .vt-rename-slot:empty { display: none; } +.vt-app .vt-banner { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-radius: var(--vt-r-sm); font-size: 12.5px; } +.vt-app .vt-banner-muted { background: var(--vt-surface-3); color: var(--vt-text-2); } + +.vt-app .vt-expcard { + display: flex; gap: 14px; align-items: center; padding: 14px; border-radius: var(--vt-r); + --vt-xc: var(--vt-ok); + background: color-mix(in oklab, var(--vt-xc) 8%, var(--vt-surface)); + border: 1px solid color-mix(in oklab, var(--vt-xc) 25%, var(--vt-line)); +} +.vt-app .vt-expcard-warn { --vt-xc: var(--vt-warn); } +.vt-app .vt-expcard-err { --vt-xc: var(--vt-err); } +.vt-app .vt-ring { position: relative; width: 58px; height: 58px; flex: none; } +.vt-app .vt-ring-svg { width: 58px; height: 58px; transform: rotate(-90deg); } +.vt-app .vt-ring-track { fill: none; stroke: color-mix(in oklab, var(--vt-xc) 18%, transparent); stroke-width: 3.5; } +.vt-app .vt-ring-arc { fill: none; stroke: var(--vt-xc); stroke-width: 3.5; stroke-linecap: round; } +.vt-app .vt-ring-n { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 17px; font-weight: 720; } +.vt-app .vt-expcard-grade { font-weight: 680; font-size: 14px; } +.vt-app .vt-expcard-reason { font-size: 12.5px; color: var(--vt-text-2); margin-top: 1px; } +.vt-app .vt-factors { margin-top: 6px; display: flex; flex-direction: column; gap: 2px; font-size: 11.5px; color: var(--vt-text-3); } +.vt-app .vt-factors li { display: flex; gap: 6px; } +.vt-app .vt-factors li .vt-tn { width: 26px; flex: none; color: var(--vt-text-2); font-weight: 650; } + +.vt-app .vt-dgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.vt-app .vt-dcard { display: flex; flex-direction: column; gap: 1px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); min-width: 0; } +.vt-app .vt-dcard-label { font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--vt-text-3); } +.vt-app .vt-dcard-value { font-size: 20px; font-weight: 700; letter-spacing: -.01em; white-space: nowrap; } +.vt-app .vt-dcard-sub { font-size: 11.5px; line-height: 1.35; color: var(--vt-text-3); min-height: 16px; } +.vt-app .vt-dcard .vt-sk { margin-top: auto; padding-top: 18px; } +.vt-app .vt-dcard .vt-sk-max { top: 16px; } +.vt-app .vt-dcard .vt-spark { height: 36px; } +.vt-app .vt-dspark { padding-top: 14px; } +.vt-app .vt-dspark .vt-spark { height: 48px; } +.vt-app .vt-dsec-title { font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .06em; color: var(--vt-text-3); margin: 0 0 6px; padding: 0; border: 0; } +.vt-app .vt-kvs { display: flex; flex-direction: column; } +.vt-app .vt-kvs .vt-kv { display: grid; grid-template-columns: 132px minmax(0, 1fr); gap: 12px; padding: 7px 0; border-bottom: 1px solid var(--vt-line); } +.vt-app .vt-kvs .vt-kv:last-child { border-bottom: 0; } +.vt-app .vt-kvs .vt-kv dt { font-size: 12px; text-transform: none; letter-spacing: 0; font-weight: 500; color: var(--vt-text-3); } +.vt-app .vt-kvs .vt-kv dd { font-size: 13px; font-weight: 520; margin: 0; white-space: pre-line; overflow-wrap: anywhere; display: flex; flex-direction: column; align-items: flex-start; gap: 1px; } +.vt-app .vt-kvs .vt-kv dd small { font-size: 11.5px; color: var(--vt-text-3); font-weight: 400; } +.vt-app .vt-dlist { display: flex; flex-direction: column; } +.vt-app .vt-dlist-name { flex: 1; min-width: 0; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + +.vt-app .vt-rename { display: flex; flex-direction: column; gap: 10px; padding: 12px; border-radius: var(--vt-r); background: var(--vt-surface-2); border: 1px solid var(--vt-line); } +.vt-app .vt-rename-label { display: flex; flex-direction: column; gap: 5px; font-size: 12px; font-weight: 600; color: var(--vt-text-2); } +.vt-app .vt-rename .vt-rename-input { + height: 36px; width: 100%; padding: 0 10px; margin: 0; + border: 1px solid var(--vt-line-2); border-radius: var(--vt-r-sm); background: var(--vt-surface); + color: var(--vt-text); font: inherit; font-size: 14px; box-shadow: none; +} +.vt-app .vt-rename .vt-rename-input:focus { outline: none; border-color: var(--vt-accent-line); box-shadow: 0 0 0 3px var(--vt-accent-soft); } +.vt-app .vt-icon-picks { display: flex; flex-wrap: wrap; gap: 4px; } +.vt-app .vt-icon-pick { + display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; margin: 0; + border: 1px solid var(--vt-line); border-radius: 8px; background: var(--vt-surface); color: var(--vt-text-2); + cursor: pointer; box-shadow: none; min-width: 0; +} +.vt-app .vt-icon-pick:hover { border-color: var(--vt-line-2); color: var(--vt-text); } +.vt-app .vt-icon-pick[aria-pressed="true"] { background: var(--vt-accent); border-color: var(--vt-accent); color: var(--vt-accent-ink); } +.vt-app .vt-rename-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; } +.vt-app .vt-rename-actions .vt-hint { flex-basis: 100%; font-size: 11px; } +.vt-app .vt-form-err { color: var(--vt-err); font-size: 12px; } + +/* ------------------------------------------------ 12. small screens, motion */ + +@container vt (max-width: 699px) { + .vt-app .vt-tile { padding: 14px 14px 16px; } + .vt-app .vt-filter { max-width: none; flex-basis: 100%; } + .vt-app .vt-key-hint { display: none; } + .vt-app .vt-fchips { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; margin: 0 -14px; padding: 0 14px; } + .vt-app .vt-fchip { flex: none; } + .vt-app .vt-head { gap: 10px; } + .vt-app .vt-headline { font-size: 18px; } + .vt-app .vt-orb { width: 32px; height: 32px; } + /* phones: a live connection is just the pulsing dot; paused or + reconnecting still says so in words */ + .vt-app .vt-live-ok { padding: 0 11px; } + .vt-app .vt-live-ok .vt-live-text { display: none; } + .vt-app .vt-livepill .vt-live-text { max-width: 110px; overflow: hidden; text-overflow: ellipsis; } + .vt-app .vt-chart .vt-tile-extra { flex-basis: 100%; justify-content: space-between; } + .vt-app .vt-health { grid-template-columns: 1fr 1fr; gap: 8px; } + .vt-app .vt-chip { padding: 9px 10px; gap: 8px; } + .vt-app .vt-chip-ico { width: 22px; height: 22px; } + .vt-app .vt-chip-reason { -webkit-line-clamp: 2; font-size: 11.5px; } + .vt-app .vt-sys-cpu { grid-template-columns: auto minmax(0, 1fr); } + .vt-app .vt-chart { --vt-mh: 108px; --vt-mg: 12px; } + .vt-app .vt-mult-now { font-size: 15px; } + .vt-app .vt-mx-odd { display: none; } + .vt-app .vt-tip { min-width: 160px; } +} + +/* clients as cards when the tile is too narrow for the table: one + column on phones, a two-up grid on tablets */ +.vt-app .vt-clients { container: vtcl / inline-size; } +@container vtcl (max-width: 919px) { + .vt-app .vt-table-wrap { margin: 0; padding: 0; overflow: visible; } + .vt-app .vt-ctable { min-width: 0; } + .vt-app .vt-ctable thead { display: none; } + .vt-app .vt-ctable { display: block; } + .vt-app .vt-ctable tbody { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 10px; } + .vt-app .vt-ctable tr.vt-row { + display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 7px 10px; align-content: start; + padding: 11px 12px; border: 1px solid var(--vt-line); border-radius: var(--vt-r); + background: var(--vt-surface); + } + .vt-app .vt-ctable td { display: block; padding: 0; border: 0; background: transparent !important; } + .vt-app .vt-row:hover { background: var(--vt-surface-2); } + .vt-app .vt-ctable tr.vt-row-sel { background: var(--vt-accent-soft); border-color: var(--vt-accent-line); } + .vt-app .vt-ctable td.vt-td-name { grid-column: 1; grid-row: 1; min-width: 0; box-shadow: none !important; } + .vt-app .vt-ctable td.vt-td-signal { grid-column: 2; grid-row: 1; justify-self: end; align-self: start; padding-top: 2px; } + .vt-app .vt-ctable td.vt-td-net { grid-column: 1; grid-row: 2; padding-left: 44px; } + .vt-app .vt-net { flex-direction: row; align-items: center; gap: 6px; flex-wrap: wrap; } + .vt-app .vt-ctable td.vt-td-live { grid-column: 2; grid-row: 2; justify-self: end; text-align: right; } + .vt-app .vt-live-cell { min-width: 0; } + .vt-app .vt-live-cell .vt-mini, .vt-app .vt-live-split { display: none; } + .vt-app .vt-ctable td.vt-td-exp { grid-column: 1 / -1; grid-row: 3; padding-left: 44px; } + .vt-app .vt-exp-text span { max-width: none; -webkit-line-clamp: 1; } + .vt-app .vt-exp { gap: 8px; } + .vt-app .vt-exp-text { flex-direction: row; align-items: baseline; gap: 6px; min-width: 0; } + .vt-app .vt-exp-text b { flex: none; } + .vt-app .vt-score { width: 28px; height: 22px; font-size: 12px; } + .vt-app .vt-td-name { min-width: 0; } + .vt-app .vt-ctable td.vt-td-rate, .vt-app .vt-ctable td.vt-td-time { display: none; } + .vt-app .vt-who-label { max-width: 100%; } + .vt-app .vt-net-ssid { font-size: 13px; max-width: none; } +} + +@media (max-width: 640px) { + .vt-app .vt-drawer { + top: auto; left: 0; right: 0; bottom: 0; width: auto; + max-height: 88vh; max-height: 88dvh; height: 88vh; height: 88dvh; + border-left: 0; border-top: 1px solid var(--vt-line); + border-radius: 18px 18px 0 0; + transform: translateY(104%); + box-shadow: 0 -18px 48px rgba(12, 14, 17, .22); + } + .vt-app .vt-drawer-grip { display: block; width: 38px; height: 4px; border-radius: 2px; background: var(--vt-line-2); margin: 8px auto 0; flex: none; } + .vt-app .vt-drawer-head { padding: 10px 12px 12px 16px; } + .vt-app .vt-drawer-body { padding: 12px 16px calc(24px + env(safe-area-inset-bottom)); } + .vt-app .vt-kvs .vt-kv { grid-template-columns: 112px minmax(0, 1fr); } +} + +@media (prefers-reduced-motion: reduce) { + .vt-app *, .vt-app *::before, .vt-app *::after { animation: none !important; transition: none !important; } + .vt-app .vt-flow-on { opacity: .8; stroke-dasharray: 1.5 6; } +} diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js new file mode 100644 index 000000000..c85db6146 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js @@ -0,0 +1,133 @@ +'use strict'; +'require baseclass'; + +/* Number and text formatting for the Vantage dashboard. Pure functions: + no DOM, no rpc. Every function accepts junk (null, strings, NaN) and + returns a placeholder instead of "NaN" or "undefined". */ + +var DASH = '—'; + +function num(v) { if (v == null || v === '') return null; v = +v; return isFinite(v) ? v : null; } + +/* 3 significant digits, at most 2 decimals: 1.23, 12.3, 123 */ +function sig3(v) { + return v >= 100 ? v.toFixed(0) : v >= 10 ? v.toFixed(1) : v.toFixed(2).replace(/\.?0+$/, '') || '0'; +} + +/* Characters that reorder or hide text (bidi controls, zero-width, C0/C1 + controls, fillers, tag characters). Client-chosen names and SSIDs may + carry them; they become a visible replacement mark. ZWJ/ZWNJ and the + emoji presentation selectors are kept. */ +var UNSAFE_TEXT = /[\u0000-\u001f\u007f-\u009f\u00ad\u034f\u061c\u115f\u1160\u17b4\u17b5\u180e\u200b\u200e\u200f\u2028-\u202e\u2060-\u206f\u3164\ufe00-\ufe0d\ufeff\uffa0\ufff9-\ufffb]|\uDB40[\uDC00-\uDC7F]/g; + +return baseclass.extend({ + DASH: DASH, + + /* bits per second -> { v: '372', u: 'Mbit/s' } */ + bitsParts: function(bps) { + bps = num(bps); + if (bps == null || bps < 0) return { v: DASH, u: '' }; + var u = [ _('bit/s'), _('kbit/s'), _('Mbit/s'), _('Gbit/s'), _('Tbit/s') ], i = 0; + while (bps >= 1000 && i < u.length - 1) { bps /= 1000; i++; } + return { v: i === 0 ? bps.toFixed(0) : sig3(bps), u: u[i] }; + }, + + bits: function(bps) { + var p = this.bitsParts(bps); + return p.u ? p.v + ' ' + p.u : p.v; + }, + + /* compact axis label: 400M, 1.5G, 20k */ + bitsShort: function(bps) { + bps = num(bps); + if (bps == null || bps <= 0) return '0'; + var u = [ '', 'k', 'M', 'G', 'T' ], i = 0; + while (bps >= 1000 && i < u.length - 1) { bps /= 1000; i++; } + return (bps >= 10 ? bps.toFixed(0) : String(+bps.toFixed(1))) + u[i]; + }, + + /* binary units for memory/storage */ + bytes: function(b) { + b = num(b); + if (b == null || b < 0) return DASH; + var u = [ _('B'), _('KiB'), _('MiB'), _('GiB'), _('TiB'), _('PiB') ], i = 0; + while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; } + return (i === 0 ? b.toFixed(0) : b >= 100 ? b.toFixed(0) : b >= 10 ? b.toFixed(1) : b.toFixed(2)) + ' ' + u[i]; + }, + + /* PHY rate from iwinfo (kbit/s) -> '576 Mbit/s' */ + phyRate: function(kbit) { + kbit = num(kbit); + if (kbit == null || kbit <= 0) return DASH; + return this.bits(kbit * 1000); + }, + + pct: function(p, digits) { + p = num(p); + if (p == null) return DASH; + return p.toFixed(digits || 0) + '%'; + }, + + /* seconds -> '5d 23h', '4h 12m', '3m 20s', '12s' */ + duration: function(s) { + s = num(s); + if (s == null || s < 0) return DASH; + s = Math.floor(s); + var d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60); + if (d) return d + 'd ' + h + 'h'; + if (h) return h + 'h ' + m + 'm'; + if (m) return m + 'm ' + (s % 60) + 's'; + return s + 's'; + }, + + /* milliseconds since -> 'just now', '12 s ago', '3 min ago' */ + ago: function(ms) { + ms = num(ms); + if (ms == null || ms < 0) return DASH; + var s = Math.round(ms / 1000); + if (s < 3) return _('just now'); + if (s < 60) return _('%d s ago').format(s); + if (s < 3600) return _('%d min ago').format(Math.floor(s / 60)); + return _('%d h ago').format(Math.floor(s / 3600)); + }, + + dbm: function(v) { + v = num(v); + if (v == null || v === 0) return DASH; + return (v < 0 ? '−' + Math.abs(v) : String(v)) + ' ' + _('dBm'); + }, + + /* load average as reported by system.info (fixed point, 65536 = 1.0) */ + load: function(v) { + v = num(v); + return v == null ? DASH : (v / 65536).toFixed(2); + }, + + /* upper-case, colon separated; anything else is returned unchanged */ + mac: function(mac) { + var s = String(mac == null ? '' : mac).trim(); + return /^([0-9a-f]{2}[:-]){5}[0-9a-f]{2}$/i.test(s) ? s.replace(/-/g, ':').toUpperCase() : s; + }, + + /* last three octets: '…00:53:C1' (the distinguishing part of a MAC) */ + macTail: function(mac) { + var m = this.mac(mac); + return /^([0-9A-F]{2}:){5}[0-9A-F]{2}$/.test(m) ? '…' + m.slice(9) : m; + }, + + /* Client- or radio-supplied text for display: invisible/reordering + characters become U+FFFD and long values are cut (by code point) with + an ellipsis. The result is still only ever used as a text node. */ + safeText: function(s, maxLen) { + if (s == null) return ''; + var chars = Array.from(String(s).replace(UNSAFE_TEXT, '\ufffd')); + maxLen = maxLen > 1 ? maxLen : 64; + return chars.length > maxLen ? chars.slice(0, maxLen - 1).join('') + '…' : chars.join(''); + }, + + /* frequency MHz -> '5975 MHz' / '5.975 GHz' is less readable; keep MHz */ + mhz: function(v) { + v = num(v); + return v ? _('%s MHz').format(v) : DASH; + } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js new file mode 100644 index 000000000..89f5ecfce --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js @@ -0,0 +1,243 @@ +'use strict'; +'require baseclass'; +'require vantage.fmt as fmt'; +'require vantage.wifi as wifi'; + +/* Explainable judgements over the model: health checks, per-client + experience, top talkers, session presence and radio insights. Every + verdict carries a one-line reason and, where something can be done + about it, the LuCI page that does it. Pure functions. */ + +var PAGES = { + network: [ 'admin', 'network', 'network' ], + wireless: [ 'admin', 'network', 'wireless' ], + processes: [ 'admin', 'status', 'processes' ], + mounts: [ 'admin', 'system', 'mounts' ], + realtime: [ 'admin', 'status', 'realtime' ] +}; + +var WEAK_DBM = -75; + +function num(v) { v = +v; return isFinite(v) ? v : 0; } +function finite(v) { return typeof v === 'number' && isFinite(v); } +function own(o, k) { return o != null && Object.prototype.hasOwnProperty.call(o, k); } +function speedShort(mbps) { return mbps >= 1000 ? (mbps / 1000) + 'G' : mbps + 'M'; } +function speedLong(mbps) { return mbps >= 1000 ? _('%s Gbit/s').format(mbps / 1000) : _('%s Mbit/s').format(mbps); } +function rank(level) { return { err: 3, warn: 2, info: 1, ok: 0 }[level] || 0; } + +return baseclass.extend({ + PAGES: PAGES, + WEAK_DBM: WEAK_DBM, + + /* ------------------------------------------------------------ health */ + + /* model + { cpu: { pct, cores } | null, uplinkRate: { rx, tx } | null } + -> [ { id, level, title, value, reason, detail, page } ] + value and reason are short enough for a health chip; detail is the + full sentence (tooltip / accessible description). */ + health: function(model, live) { + model = model || {}; + live = live || {}; + var out = [], dev = model.device || {}, up = model.uplink; + + /* uplink: live traffic is the value, the negotiated link the context */ + if (!up) out.push({ id: 'uplink', level: 'err', title: _('Uplink'), value: _('Down'), reason: _('No interface with a route'), + detail: _('No network interface with a route is up'), page: PAGES.network }); + else if (up.carrier === false) out.push({ id: 'uplink', level: 'err', title: _('Uplink'), value: _('No link'), reason: _('%s: no carrier').format(up.dev), + detail: _('%s has no carrier: cable unplugged or switch port down').format(up.dev), page: PAGES.network }); + else if (up.speed && up.speed.duplex === 'half') out.push({ id: 'uplink', level: 'warn', title: _('Uplink'), value: _('Half duplex'), reason: _('%s · check cable').format(up.dev), + detail: _('%s negotiated half duplex; check the cable and switch port').format(up.dev), page: PAGES.network }); + else if (up.speed && up.speed.mbps < 1000) out.push({ id: 'uplink', level: 'warn', title: _('Uplink'), value: _('%s Mbit/s').format(up.speed.mbps), reason: _('%s · slow link').format(up.dev), + detail: _('%s negotiated only %d Mbit/s; a cable pair or port may be faulty').format(up.dev, up.speed.mbps), page: PAGES.network }); + else { + var rate = live.uplinkRate && finite(live.uplinkRate.rx) && finite(live.uplinkRate.tx) ? live.uplinkRate : null; + /* no-break spaces keep '↑ 2.9 Mbit/s' and '2.5G link' whole when the chip wraps */ + var link = up.speed ? _('%s link').format(speedShort(up.speed.mbps)).replace(/ /g, '\u00a0') : null; + out.push({ id: 'uplink', level: 'ok', title: _('Uplink'), value: rate ? '↓ ' + fmt.bits(rate.rx) : _('Up'), + reason: [ rate ? ('↑ ' + fmt.bits(rate.tx)).replace(/ /g, '\u00a0') : null, up.dev, link ].filter(Boolean).join(' · '), + detail: (up.speed ? _('%s up at %s').format(up.dev, speedLong(up.speed.mbps)) : _('%s up').format(up.dev)) + + (up.gateway ? ', ' + _('gateway %s').format(up.gateway) : '') + + (rate ? '; ' + _('now %s down, %s up').format(fmt.bits(rate.rx), fmt.bits(rate.tx)) : ''), + page: PAGES.network }); + } + + /* CPU */ + var cpu = live.cpu && finite(live.cpu.pct) ? live.cpu.pct : null, cores = (live.cpu && live.cpu.cores && live.cpu.cores.length) || 1; + var load = (dev.load || [])[0]; + if (cpu == null) out.push({ id: 'cpu', level: 'info', title: _('CPU'), value: fmt.DASH, reason: _('Measuring…'), detail: _('Measuring over the next poll'), page: PAGES.processes }); + else out.push({ id: 'cpu', level: cpu >= 90 ? 'err' : cpu >= 70 ? 'warn' : 'ok', title: _('CPU'), value: fmt.pct(cpu), + reason: cpu >= 70 ? _('Busy · load %s').format(fmt.load(load)) : _('Load %s · %d cores').format(fmt.load(load), cores), + detail: cpu >= 70 ? _('Busy; load %s on %d cores; see the busiest processes').format(fmt.load(load), cores) : _('Load %s on %d cores').format(fmt.load(load), cores), + page: PAGES.processes }); + + /* memory, framed as available (as in the System tile) */ + var mem = dev.memory; + if (mem) { + var avail = finite(mem.availPct) ? mem.availPct : 100 - num(mem.usedPct); + out.push({ id: 'memory', level: mem.usedPct >= 90 ? 'err' : mem.usedPct >= 80 ? 'warn' : 'ok', title: _('Memory'), value: _('%s available').format(fmt.pct(avail)), + reason: _('%s of %s').format(fmt.bytes(mem.available), fmt.bytes(mem.total)), + detail: _('%s of %s available (%s in use)').format(fmt.bytes(mem.available), fmt.bytes(mem.total), fmt.pct(mem.usedPct)), page: PAGES.processes }); + } + + /* radios; an SSID without clients is normal, not a fault */ + var radios = model.radios || [], nSsid = (model.ssids || []).length; + var off = radios.filter(function(r) { return r.disabled; }), down = radios.filter(function(r) { return !r.disabled && !r.up; }); + var idle = (model.ssids || []).filter(function(s) { return s.up && !s.clients; }); + if (!radios.length) out.push({ id: 'radios', level: 'info', title: _('Wi-Fi'), value: _('None'), reason: _('No radios reported'), detail: _('This device reports no radios'), page: PAGES.wireless }); + else if (down.length) out.push({ id: 'radios', level: 'err', title: _('Wi-Fi'), value: _('%d down').format(down.length), reason: _('%s is not up').format(down[0].bandLabel), + detail: _('%s (%s) is enabled but not up').format(down[0].id, down[0].bandLabel), page: PAGES.wireless }); + else if (off.length) out.push({ id: 'radios', level: 'warn', title: _('Wi-Fi'), value: _('%d off').format(off.length), reason: _('%s is disabled').format(off[0].bandLabel), + detail: _('%s (%s) is disabled').format(off[0].id, off[0].bandLabel), page: PAGES.wireless }); + else out.push({ id: 'radios', level: 'ok', title: _('Wi-Fi'), value: _('%d up').format(radios.length), + reason: idle.length ? _('%d SSIDs · %d idle').format(nSsid, idle.length) : _('%d SSIDs on air').format(nSsid), + detail: idle.length ? _('%d SSIDs broadcasting; %d without clients, which is fine').format(nSsid, idle.length) : _('%d SSIDs broadcasting').format(nSsid), + page: PAGES.wireless }); + + /* signal */ + var clients = model.clients || []; + var weak = clients.filter(function(c) { return c.signal != null && c.signal < WEAK_DBM; }).sort(function(a, b) { return a.signal - b.signal; }); + if (!clients.length) out.push({ id: 'signal', level: 'info', title: _('Signal'), value: _('No clients'), reason: _('Nobody associated'), detail: _('No stations are associated right now'), page: PAGES.wireless }); + else if (weak.length) out.push({ id: 'signal', level: 'warn', title: _('Signal'), value: _('%d weak').format(weak.length), + reason: _('%s · %s').format(weak[0].name, fmt.dbm(weak[0].signal)), + detail: _('%s at %s; below %s roaming and throughput suffer').format(weak[0].name, fmt.dbm(weak[0].signal), fmt.dbm(WEAK_DBM)), page: PAGES.wireless, client: weak[0].mac }); + else out.push({ id: 'signal', level: 'ok', title: _('Signal'), value: _('All good'), reason: _('All above %s').format(fmt.dbm(WEAK_DBM)), + detail: _('All %d clients above %s').format(clients.length, fmt.dbm(WEAK_DBM)), page: PAGES.wireless }); + + /* storage */ + var root = (dev.storage || [])[0]; + if (root) out.push({ id: 'storage', level: root.usedPct >= 95 ? 'err' : root.usedPct >= 85 ? 'warn' : 'ok', title: _('Storage'), value: _('%s used').format(fmt.pct(root.usedPct)), + reason: _('%s free').format(fmt.bytes(root.free)), detail: _('%s free on the overlay (%s used)').format(fmt.bytes(root.free), fmt.pct(root.usedPct)), page: PAGES.mounts }); + + return out; + }, + + /* -> { level, count (warn+err), headline } */ + summary: function(checks) { + var worst = 'ok', n = 0; + (checks || []).forEach(function(c) { + if (rank(c.level) > rank(worst)) worst = c.level; + if (c.level === 'warn' || c.level === 'err') n++; + }); + var headline = n === 0 ? _('Everything looks healthy') : n === 1 ? _('1 thing needs attention') : _('%d things need attention').format(n); + return { level: worst === 'info' ? 'ok' : worst, count: n, headline: headline }; + }, + + /* -------------------------------------------------------- experience */ + + /* Per-client experience 0..100 from signal, link rate, delivery + failures and the generation gap to the radio. The explanation is + the factor that cost the most. -> { score, grade, level, reason, short, factors } */ + experience: function(c) { + c = c || {}; + var factors = [], s = c.signal; + if (s == null) factors.push({ cost: 0, text: _('Signal not reported'), short: _('Signal not reported') }); + else if (s < -80) factors.push({ cost: 70, text: _('Very weak signal %s; expect drops').format(fmt.dbm(s)), short: _('Very weak signal; expect drops') }); + else if (s < WEAK_DBM) factors.push({ cost: 55, text: _('Weak signal %s; may roam poorly').format(fmt.dbm(s)), short: _('Weak signal') }); + else if (s < -67) factors.push({ cost: 30, text: _('Fair signal %s; lower rates at range').format(fmt.dbm(s)), short: _('Fair signal') }); + else if (s < -60) factors.push({ cost: 10, text: _('Good signal %s').format(fmt.dbm(s)), short: _('Good signal') }); + + var best = Math.max(c.rx && c.rx.rate || 0, c.tx && c.tx.rate || 0); + var active = c.inactive == null || c.inactive < 10000; + if (best > 0 && best < 20000 && active && s != null && s >= -70) + factors.push({ cost: 20, text: _('Low link rate %s despite good signal; interference or an old device').format(fmt.phyRate(best)), short: _('Low link rate for its signal') }); + + var sent = num(c.txPackets) + num(c.txFailed); + if (sent >= 200) { + var fail = num(c.txFailed) / sent * 100; + if (fail >= 10) factors.push({ cost: 25, text: _('%s of frames to the device fail; interference likely').format(fmt.pct(fail)), short: _('%s of frames fail').format(fmt.pct(fail)) }); + else if (fail >= 4) factors.push({ cost: 10, text: _('%s of frames to the device fail').format(fmt.pct(fail)), short: _('%s of frames fail').format(fmt.pct(fail)) }); + } + + if (c.legacyOnRadio) + factors.push({ cost: 5, text: _('%s device on a newer radio; uses more airtime per byte').format(c.gen === 'legacy' ? _('Legacy') : c.gen), + short: _('Older %s device').format(c.gen === 'legacy' ? _('legacy') : c.gen) }); + + var cost = factors.reduce(function(a, f) { return a + f.cost; }, 0); + var score = Math.max(0, Math.min(100, 100 - cost)); + var grade = score >= 85 ? _('Excellent') : score >= 65 ? _('Good') : score >= 40 ? _('Fair') : _('Poor'); + var level = score >= 65 ? 'ok' : score >= 40 ? 'warn' : 'err'; + var worst = factors.slice().sort(function(a, b) { return b.cost - a.cost; })[0]; + var reason = (worst && worst.cost > 0) ? worst.text + : (s != null ? _('Strong signal %s, %s link').format(fmt.dbm(s), fmt.phyRate(best)) : _('No problems detected')); + var short = (worst && worst.cost > 0) ? worst.short + : (s != null ? (best > 0 ? _('Strong signal, %s').format(fmt.phyRate(best)) : _('Strong signal')) : _('No problems detected')); + return { score: score, grade: grade, level: level, reason: reason, short: short, factors: factors }; + }, + + /* ------------------------------------------------------ top talkers */ + + /* clients + rates { mac: { rx, tx } } (AP view: tx = to the client) + -> top n [ { mac, name, down, up, total, share } ] with total > 0 */ + topTalkers: function(clients, rates, n) { + var list = [], sum = 0; + (clients || []).forEach(function(c) { + var r = own(rates, c.mac) ? rates[c.mac] : null; + if (!r) return; + var down = num(r.tx), up = num(r.rx), total = down + up; + if (!(total > 0)) return; + sum += total; + list.push({ mac: c.mac, name: c.name, down: down, up: up, total: total }); + }); + list.sort(function(a, b) { return b.total - a.total || (a.mac < b.mac ? -1 : 1); }); + list.forEach(function(t) { t.share = sum > 0 ? t.total / sum : 0; }); + return list.slice(0, n > 0 ? n : 5); + }, + + /* --------------------------------------------------------- presence */ + + /* Session presence. state: { baseline: [macs] | null, present: { mac: client }, + gone: { mac: { client, at } }, firstSeen: { mac: ms } }. + The first call fixes the baseline (what was there when the page + opened); later arrivals are "new". Stations that leave are kept as + gone for keepMs. -> { state, isNew: { mac: true }, gone: [ { client, at } ] } */ + presence: function(state, clients, now, keepMs) { + state = state || {}; + var baseline = Array.isArray(state.baseline) ? state.baseline : null; + var present = Object.create(null), gone = Object.create(null), firstSeen = Object.create(null), isNew = Object.create(null); + var prevPresent = state.present || {}, prevGone = state.gone || {}, prevFirst = state.firstSeen || {}; + + (clients || []).forEach(function(c) { + present[c.mac] = c; + firstSeen[c.mac] = own(prevFirst, c.mac) ? prevFirst[c.mac] : now; + if (baseline && baseline.indexOf(c.mac) < 0) isNew[c.mac] = true; + }); + Object.keys(prevGone).forEach(function(mac) { + var g = prevGone[mac]; + if (!own(present, mac) && g && now - g.at < keepMs) { gone[mac] = g; firstSeen[mac] = prevFirst[mac]; } + }); + Object.keys(prevPresent).forEach(function(mac) { + if (!own(present, mac)) { gone[mac] = { client: prevPresent[mac], at: now }; firstSeen[mac] = prevFirst[mac]; } + }); + + var goneList = Object.keys(gone).map(function(k) { return gone[k]; }).sort(function(a, b) { return b.at - a.at; }); + return { + state: { baseline: baseline || Object.keys(present), present: present, gone: gone, firstSeen: firstSeen }, + isNew: isNew, + gone: goneList + }; + }, + + /* ---------------------------------------------------------- radios */ + + /* radios + per-radio rates { id: { rx, tx } } -> { id: { busiest, share, notes: [ { level, text } ] } } */ + radioInsights: function(radios, rates) { + var out = {}, sum = 0, top = null, topV = 0; + (radios || []).forEach(function(r) { + var v = own(rates, r.id) ? num(rates[r.id].rx) + num(rates[r.id].tx) : 0; + sum += v; + if (v > topV) { topV = v; top = r.id; } + }); + (radios || []).forEach(function(r) { + var v = own(rates, r.id) ? num(rates[r.id].rx) + num(rates[r.id].tx) : 0; + var notes = []; + if (r.disabled) notes.push({ level: 'warn', text: _('Radio is disabled') }); + else if (!r.up) notes.push({ level: 'err', text: _('Radio is enabled but not up') }); + if (top === r.id && sum > 0 && (radios.length > 1)) notes.push({ level: 'info', text: _('Busiest radio: %s of Wi-Fi traffic').format(fmt.pct(v / sum * 100)) }); + if (r.legacy) notes.push({ level: 'info', text: r.legacy === 1 ? _('1 older device (Wi-Fi 4 or earlier) slows airtime') : _('%d older devices (Wi-Fi 4 or earlier) slow airtime').format(r.legacy) }); + if (r.noise == null && r.noiseRaw != null) notes.push({ level: 'muted', text: _('Noise floor not reported by the driver') }); + if (r.up && !r.clients) notes.push({ level: 'muted', text: _('No clients; idle radios are fine') }); + out[r.id] = { busiest: top === r.id && sum > 0, share: sum > 0 ? v / sum : 0, notes: notes }; + }); + return out; + } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js new file mode 100644 index 000000000..8c4158e01 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/model.js @@ -0,0 +1,488 @@ +'use strict'; +'require baseclass'; +'require vantage.fmt as fmt'; +'require vantage.wifi as wifi'; +'require vantage.names as names'; + +/* Turns raw ubus replies into one normalised model of the device, its + uplink, radios, SSIDs and stations, plus the counter/rate arithmetic. + Pure functions: the view owns all state and passes it in. */ + +/* interface names that are wireless even when netifd reports no devtype + (it does not on ipq53xx/ath12k: phy2g-ap0, phy6g-ap0, ...) */ +var WIRELESS_NAME = /^(wlan\d|phy[0-9a-z]*-|ath\d|wl\d|ra\d|rai\d|rax\d|mld\d)/; + +/* above any real link: a rate this high means a bad sample, not traffic */ +var MAX_BPS = 400e9; + +function num(v) { v = +v; return isFinite(v) ? v : 0; } +function finite(v) { return typeof v === 'number' && isFinite(v); } +function own(o, k) { return o != null && Object.prototype.hasOwnProperty.call(o, k); } +function obj(v) { return (v && typeof v === 'object' && !Array.isArray(v)) ? v : {}; } +function arr(v) { return Array.isArray(v) ? v : []; } + +return baseclass.extend({ + /* ------------------------------------------------------------- CPU */ + + /* /proc/stat -> { all: { idle, total }, cores: [ { idle, total } ] } */ + parseStat: function(text) { + var out = { all: null, cores: [] }; + String(text || '').split('\n').forEach(function(line) { + var m = line.match(/^cpu(\d*)\s+([\d\s]+)$/); + if (!m) return; + var f = m[2].trim().split(/\s+/).map(num); + /* user nice system idle iowait irq softirq steal */ + var idle = f[3] + (f[4] || 0), total = 0; + for (var i = 0; i < Math.min(f.length, 8); i++) total += f[i]; + if (m[1] === '') out.all = { idle: idle, total: total }; + else out.cores[+m[1]] = { idle: idle, total: total }; + }); + out.cores = out.cores.filter(Boolean); + return out.all ? out : null; + }, + + cpuPercent: function(prev, cur) { + if (!prev || !cur) return null; + var dt = cur.total - prev.total, di = cur.idle - prev.idle; + if (!(dt > 0)) return null; + return Math.max(0, Math.min(100, (1 - di / dt) * 100)); + }, + + /* { pct, cores: [ pct, ... ] } between two parseStat() results */ + cpuUsage: function(prev, cur) { + if (!prev || !cur) return null; + var self = this, pct = this.cpuPercent(prev.all, cur.all); + if (pct == null) return null; + return { pct: pct, cores: cur.cores.map(function(c, i) { return self.cpuPercent(prev.cores[i], c); }) }; + }, + + /* ---------------------------------------------------------- memory */ + + /* One bar, three segments that add up to total: + In use total - available + Cache (reclaimable) available - free (page cache, buffers) + Free free + 'available' is the kernel's estimate (MemAvailable); without it + free + buffered + cached stands in. */ + memory: function(m) { + m = obj(m); + var total = num(m.total); + if (!(total > 0)) return null; + var free = Math.min(Math.max(num(m.free), 0), total); + var avail = finite(m.available) ? m.available : free + num(m.buffered) + num(m.cached); + avail = Math.min(Math.max(avail, free), total); + var used = total - avail, cache = avail - free; + return { + total: total, available: avail, used: used, cache: cache, free: free, + availPct: avail / total * 100, usedPct: used / total * 100, + segments: [ + { key: 'used', label: _('In use'), bytes: used, pct: used / total * 100 }, + { key: 'cache', label: _('Cache (reclaimable)'), bytes: cache, pct: cache / total * 100 }, + { key: 'free', label: _('Free'), bytes: free, pct: free / total * 100 } + ] + }; + }, + + /* system.info swap -> { total, used, free, usedPct } or null when none */ + swap: function(s) { + s = obj(s); + var total = num(s.total), free = Math.min(Math.max(num(s.free), 0), total); + if (!(total > 0)) return null; + return { total: total, free: free, used: total - free, usedPct: (total - free) / total * 100 }; + }, + + /* system.info root/tmp (KiB) -> bytes { total, used, free, usedPct } */ + mount: function(s, label) { + s = obj(s); + var total = num(s.total) * 1024; + if (!(total > 0)) return null; + var used = Math.min(Math.max(num(s.used) * 1024, 0), total); + var free = finite(s.avail) ? Math.min(num(s.avail) * 1024, total - used) : total - used; + return { label: label, total: total, used: used, free: free, usedPct: used / total * 100 }; + }, + + /* ---------------------------------------------------------- counters */ + + /* Rate between two byte-counter samples { at: ms, rx, tx }. + 'first': no previous sample; 'skip': under 1 s apart (keep prev as + the base); 'reset': a counter went backwards or the rate is not + physical (restart from cur); 'ok': rx/tx in bit/s. */ + counterRate: function(prev, cur) { + if (!prev || !cur || !finite(prev.at) || !finite(cur.at)) return { state: 'first' }; + var dt = (cur.at - prev.at) / 1000; + if (!(dt >= 1)) return { state: 'skip' }; + var rx = (num(cur.rx) - num(prev.rx)) * 8 / dt, tx = (num(cur.tx) - num(prev.tx)) * 8 / dt; + if (!(rx >= 0 && tx >= 0 && rx <= MAX_BPS && tx <= MAX_BPS)) return { state: 'reset' }; + return { state: 'ok', rx: rx, tx: tx, dt: dt }; + }, + + /* Capture time for one poll's counters. The browser clock is precise + but only says when the reply arrived; system.info localtime (whole + seconds) says which sample the device served. Same localtime as + last poll: the same snapshot again (-> dt 0, the rate is skipped). + The two clocks disagree by more than 2 s (a delayed reply, a + replayed or skipped sample): trust the device's spacing. + prev/next: { at: ms, local: s } */ + sampleClock: function(prev, now, local) { + if (!finite(local)) return { at: now, local: null }; + if (!prev || !finite(prev.at) || !finite(prev.local)) return { at: now, local: local }; + var dLocal = (local - prev.local) * 1000, dClient = now - prev.at; + if (dLocal === 0) return { at: prev.at, local: local }; + if (dLocal < 0 || Math.abs(dLocal - dClient) > 2000) return { at: prev.at + Math.max(dLocal, 0) || now, local: local }; + return { at: now, local: local }; + }, + + /* Rates for a set of counters { key: { rx, tx } } captured at `at`. + Returns { next, rates }; next holds only keys present now, so the + state cannot grow with every station that ever associated. */ + rates: function(prevMap, counters, at) { + var next = Object.create(null), rates = Object.create(null), self = this; + Object.keys(obj(counters)).forEach(function(k) { + var c = counters[k]; + if (!c) return; + var p = own(prevMap, k) ? prevMap[k] : null; + var cur = { at: at, rx: num(c.rx), tx: num(c.tx) }; + var r = self.counterRate(p, cur); + if (r.state === 'ok') rates[k] = { rx: r.rx, tx: r.tx }; + next[k] = r.state === 'skip' ? p : cur; + }); + return { next: next, rates: rates }; + }, + + /* ------------------------------------------------------------ uplink */ + + isWireless: function(name, dev, known) { + if (known && known.indexOf(name) >= 0) return true; + if (dev && (dev.devtype === 'wlan' || dev.wireless === true)) return true; + return WIRELESS_NAME.test(String(name || '')); + }, + + parseSpeed: function(s) { + var m = String(s == null ? '' : s).match(/^(\d+)([FH])?/); + if (!m || +m[1] <= 0) return null; + return { mbps: +m[1], duplex: m[2] === 'H' ? 'half' : (m[2] === 'F' ? 'full' : null) }; + }, + + speedLabel: function(sp) { + if (!sp) return fmt.DASH; + var v = sp.mbps >= 1000 ? _('%s Gbit/s').format(sp.mbps / 1000) : _('%s Mbit/s').format(sp.mbps); + return v + (sp.duplex === 'full' ? ' · ' + _('full duplex') : sp.duplex === 'half' ? ' · ' + _('half duplex') : ''); + }, + + /* Pick the interface carrying traffic in/out of this device, and the + physical device its counters live on. Routers: wan/wan6/*wan*. + Access points: the interface with the default route, else the first + one that is up. A bridge is followed to its wired member (wireless + members are skipped; netifd 'ethernet' members preferred); DSA with + hardware offload uses the conduit's counters. */ + resolveUplink: function(ifaces, devs, wirelessNames) { + var self = this; + ifaces = arr(ifaces); + devs = obj(devs); + + var up = ifaces.filter(function(i) { + return i && i.up && i.interface !== 'loopback' && (i.l3_device || i.device); + }); + function named(re) { return up.filter(function(i) { return re.test(i.interface); })[0]; } + function defRoute(i) { + return arr(i.route).filter(function(r) { + return r && +r.mask === 0 && (r.target === '0.0.0.0' || r.target === '::'); + })[0] || null; + } + + var wan = named(/^wan$/) || named(/^wan6$/) || named(/wan/i); + var pick = wan || up.filter(defRoute)[0] || up[0]; + if (!pick) return null; + + var l3 = pick.l3_device || pick.device, dev = l3, d = devs[dev]; + if (d && (d.type === 'bridge' || d.devtype === 'bridge') && Array.isArray(d['bridge-members'])) { + var members = d['bridge-members']; + var wired = members.filter(function(m) { return !self.isWireless(m, devs[m], wirelessNames); }); + var eth = wired.filter(function(m) { return devs[m] && devs[m].devtype === 'ethernet'; }); + var next = eth[0] || wired[0] || members[0]; + if (next) { dev = next; d = devs[dev] || null; } + } + + var statsDev = dev; + if (d && d.devtype === 'dsa' && d['hw-tc-offload'] && d.conduit && devs[d.conduit]) + statsDev = d.conduit; + + var route = defRoute(pick); + return { + iface: pick.interface, + l3dev: l3, + dev: dev, + statsDev: statsDev, + isWan: !!wan, + proto: pick.proto || null, + uptime: finite(pick.uptime) ? pick.uptime : null, + up: !!pick.up, + carrier: d ? d.carrier !== false : null, + speed: d ? this.parseSpeed(d.speed) : null, + ipv4: arr(pick['ipv4-address']).map(function(a) { return a && a.address ? a.address + '/' + a.mask : null; }).filter(Boolean), + ipv6: arr(pick['ipv6-address']).map(function(a) { return a && a.address ? a.address + '/' + a.mask : null; }).filter(Boolean), + gateway: route && route.nexthop && route.nexthop !== '0.0.0.0' ? route.nexthop : null, + dns: arr(pick['dns-server']).filter(function(s) { return typeof s === 'string'; }), + stats: d && d.statistics ? d.statistics : null + }; + }, + + /* The device tree model is sometimes the SoC reference design + ("Qualcomm Technologies, Inc. IPQ5332/RDP442/..."); board_name + ("zyxel,nwa50be") names the actual product then. */ + deviceModel: function(board) { + board = obj(board); + var model = String(board.model || ''), m = String(board.board_name || '').match(/^([^,]+),(.+)$/); + if (m && (!model || /Qualcomm Technologies|\bRDP\d|reference board|\bEVB\b|\bRFB\b/i.test(model))) + return m[1].charAt(0).toUpperCase() + m[1].slice(1) + ' ' + m[2].toUpperCase(); + return model || board.board_name || null; + }, + + /* ------------------------------------------------ address filtering */ + + parseIPv4: function(s) { + var m = typeof s === 'string' ? s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/) : null; + if (!m) return null; + var out = []; + for (var i = 1; i <= 4; i++) { + if (m[i].length > 1 && m[i].charAt(0) === '0') return null; + if (+m[i] > 255) return null; + out.push(+m[i]); + } + return out; + }, + + ipv4InSubnet: function(ip, net, mask) { + var a = this.parseIPv4(ip), b = this.parseIPv4(net); + mask = +mask; + if (!a || !b || !(mask >= 0 && mask <= 32)) return false; + for (var i = 0; i < 4; i++) { + var bits = Math.max(0, Math.min(8, mask - i * 8)); + if (!bits) break; + var m = (0xff << (8 - bits)) & 0xff; + if ((a[i] & m) !== (b[i] & m)) return false; + } + return true; + }, + + /* host-hint IPv4 addresses inside one of the given subnets + ([ 'a.b.c.d/nn', ... ]); hints are keyed by MAC only, so a hint may + name any address. IPv6 link-local and global addresses are kept. */ + hintAddrs: function(hint, subnets) { + var self = this, out = []; + hint = obj(hint); + arr(hint.ipaddrs).forEach(function(ip) { + if (typeof ip !== 'string' || out.indexOf(ip) >= 0) return; + if (!subnets.length || subnets.some(function(s) { var p = s.split('/'); return +p[1] >= 8 && self.ipv4InSubnet(ip, p[0], p[1]); })) + out.push(ip); + }); + arr(hint.ip6addrs).forEach(function(ip) { + if (typeof ip === 'string' && /^[0-9a-f:]+$/i.test(ip) && ip.length <= 45 && out.indexOf(ip) < 0) out.push(ip); + }); + return out; + }, + + /* ------------------------------------------------------------ model */ + + /* raw: { board, info, ifaces, devs, wifi, iwinfo: { ifname: info }, + assoc: { ifname: [ station ] }, hapd: { ifname: { clients } }, + hapdStatus: { ifname: status }, hints, aliases, rdns, mdns, + vendor: fn(mac) -> name|null } */ + build: function(raw) { + raw = raw || {}; + var self = this, board = obj(raw.board), info = obj(raw.info), devs = obj(raw.devs), hints = obj(raw.hints); + var wifiDevs = obj(raw.wifi), iw = obj(raw.iwinfo), assoc = obj(raw.assoc), hapd = obj(raw.hapd), hstat = obj(raw.hapdStatus); + + var radios = [], ssids = [], clients = [], wlNames = []; + + Object.keys(wifiDevs).sort().forEach(function(rname) { + var r = obj(wifiDevs[rname]), cfg = obj(r.config), rIw = obj(r.iwinfo); + var ifs = arr(r.interfaces).filter(function(i) { return i && typeof i === 'object'; }); + var firstIf = ifs.filter(function(i) { return i.ifname && own(iw, i.ifname); })[0]; + var info0 = firstIf ? obj(iw[firstIf.ifname]) : {}; + var band = wifi.bandKey(cfg.band, info0.frequency || rIw.frequency); + var ht = wifi.htmode(info0.htmode || cfg.htmode); + var chan = num(info0.channel || rIw.channel || cfg.channel) || null; + var radio = { + id: rname, + band: band, + bandLabel: wifi.bandLabel(band), + up: r.up === true, + disabled: r.disabled === true || cfg.disabled === true || cfg.disabled === '1', + pending: r.pending === true, + channel: chan, + auto: cfg.channel === 'auto', + centre: num(info0.center_chan1) || null, + freq: num(info0.frequency || rIw.frequency) || null, + width: ht.width, + std: ht.std, + gen: wifi.genName(ht.std, band), + htmode: info0.htmode || cfg.htmode || null, + txpower: finite(info0.txpower) ? info0.txpower : (finite(rIw.txpower) ? rIw.txpower : null), + txpowerCfg: cfg.txpower != null && isFinite(+cfg.txpower) ? +cfg.txpower : null, + noise: wifi.noise(finite(info0.noise) ? info0.noise : rIw.noise), + noiseRaw: finite(info0.noise) ? info0.noise : (finite(rIw.noise) ? rIw.noise : null), + country: cfg.country || rIw.country || null, + hwmodes: arr(rIw.hwmodes || info0.hwmodes).join('/'), + airtime: null, + ifnames: [], + ssids: [], + clients: 0, + legacy: 0 + }; + radio.spectrum = wifi.spectrum(band, chan, radio.centre, radio.width); + + ifs.forEach(function(i) { + var ic = obj(i.config), ifname = typeof i.ifname === 'string' ? i.ifname : null; + /* luci.vantage wireless has no iwinfo part: the view's + per-interface iwinfo info replies stand in for it */ + var iiw = obj(i.iwinfo || (ifname && own(iw, ifname) ? iw[ifname] : null)); + if (ifname) { radio.ifnames.push(ifname); wlNames.push(ifname); } + var st = ifname ? obj(hstat[ifname]) : {}; + var air = obj(st.airtime); + if (num(air.time) > 0 || num(air.utilization) > 0) + radio.airtime = Math.max(radio.airtime || 0, Math.min(100, num(air.utilization) / 2.55)); + var ss = { + id: ifname || (rname + ':' + (i.section || ssids.length)), + ifname: ifname, + section: typeof i.section === 'string' ? i.section : null, + radio: rname, + band: band, + ssid: fmt.safeText(ic.ssid || iiw.ssid || '', 32), + mode: ic.mode || 'ap', + hidden: ic.hidden === true || ic.hidden === '1', + security: wifi.security(ic.encryption), + pmf: +ic.ieee80211w || 0, + networks: arr(ic.network), + bssid: fmt.mac(iiw.bssid || ic.macaddr || ''), + up: radio.up && !!ifname && (!devs[ifname] || devs[ifname].up !== false), + clients: 0 + }; + radio.ssids.push(ss.id); + ssids.push(ss); + }); + radios.push(radio); + }); + + var uplink = this.resolveUplink(raw.ifaces, devs, wlNames); + var subnets = uplink ? uplink.ipv4 : []; + + function hintOf(mac) { return obj(hints[mac.toUpperCase()] || hints[mac]); } + + /* mDNS names only where they are unambiguous across all stations */ + var everyone = []; + ssids.forEach(function(ss) { + if (ss.ifname) arr(assoc[ss.ifname]).forEach(function(s) { + var mac = s && names.normMac(s.mac); + if (mac) everyone.push({ mac: mac, ips: self.hintAddrs(hintOf(mac), subnets) }); + }); + }); + var mdns = names.mdnsForStations(raw.mdns, everyone); + + ssids.forEach(function(ss) { + if (!ss.ifname) return; + var hc = obj(obj(hapd[ss.ifname]).clients); + var hmap = Object.create(null); + Object.keys(hc).forEach(function(k) { var m = names.normMac(k); if (m) hmap[m] = obj(hc[k]); }); + arr(assoc[ss.ifname]).forEach(function(s) { + var mac = s && names.normMac(s.mac); + if (!mac) return; + var h = hmap[mac] || {}, rx = obj(s.rx), tx = obj(s.tx); + var hint = hintOf(mac); + var ips = self.hintAddrs(hint, subnets); + var std = wifi.clientStd(rx, tx, h); + var alias = raw.aliases && raw.aliases[mac]; + var nm = names.resolve({ + mac: mac, alias: alias, ips: ips, rdns: raw.rdns, mdns: mdns, + hint: hint, wps: names.wpsName(h.signature), + vendor: typeof raw.vendor === 'function' ? raw.vendor(names.ouiKey(mac)) : null + }); + var radio = radios.filter(function(r) { return r.id === ss.radio; })[0]; + var c = { + mac: mac, + macLabel: fmt.mac(mac), + name: nm.name, + nameSource: nm.source, + nameSourceLabel: nm.sourceLabel, + icon: nm.icon, + secondary: nm.secondary, + isPrivate: nm.isPrivate, + alias: alias ? alias.name : null, + aliasSid: alias ? alias.sid : null, + vendor: nm.source === 'vendor' ? (typeof raw.vendor === 'function' ? raw.vendor(names.ouiKey(mac)) : null) : null, + wps: names.wpsName(h.signature), + ips: ips, + ifname: ss.ifname, + ssid: ss.ssid, + ssidId: ss.id, + radio: ss.radio, + band: ss.band, + bandLabel: wifi.bandLabel(ss.band), + signal: finite(s.signal) && s.signal < 0 ? s.signal : null, + signalAvg: finite(s.signal_avg) && s.signal_avg < 0 ? s.signal_avg : null, + noise: wifi.noise(s.noise), + std: std, + gen: wifi.genName(std, ss.band), + rx: wifi.phy(rx), + tx: wifi.phy(tx), + rxBytes: num(rx.bytes), txBytes: num(tx.bytes), + rxPackets: num(rx.packets), txPackets: num(tx.packets), + txRetries: num(tx.retries), txFailed: num(tx.failed), + connected: finite(s.connected_time) ? s.connected_time : null, + inactive: finite(s.inactive) ? s.inactive : null, + authorized: s.authorized !== false, + mfp: !!(s.mfp || h.mfp), + wmm: !!(s.wme || h.wmm), + caps: { ht: !!h.ht, vht: !!h.vht, he: !!h.he, eht: !!h.eht, mbo: !!h.mbo, rrm: arr(h.rrm).some(function(v) { return +v > 0; }) } + }; + c.level = wifi.signal(c.signal); + c.legacyOnRadio = !!(radio && radio.std && wifi.genRank(c.gen) >= 0 && wifi.genRank(c.gen) < wifi.genRank(wifi.genName(radio.std, radio.band)) && + wifi.genRank(c.gen) <= 4 && wifi.genRank(wifi.genName(radio.std, radio.band)) >= 6); + if (radio) { radio.clients++; if (c.legacyOnRadio) radio.legacy++; } + ss.clients++; + clients.push(c); + }); + }); + + var mem = this.memory(info.memory); + var release = obj(board.release); + return { + device: { + model: this.deviceModel(board) || _('OpenWrt device'), + hostname: fmt.safeText(board.hostname || '', 64) || null, + firmware: release.description || [ release.distribution, release.version ].filter(Boolean).join(' ') || null, + version: release.version || null, + target: release.target || null, + kernel: board.kernel || null, + system: board.system || null, + uptime: finite(info.uptime) ? info.uptime : null, + localtime: finite(info.localtime) ? info.localtime : null, + load: arr(info.load).slice(0, 3).map(num), + memory: mem, + swap: this.swap(info.swap), + storage: [ this.mount(info.root, _('Root (overlay)')), this.mount(info.tmp, _('Temporary (RAM)')) ].filter(Boolean) + }, + uplink: uplink, + radios: radios, + ssids: ssids, + clients: clients + }; + }, + + /* SSIDs with the same name and security across radios -> one entry + { key, ssid, security, bands: [..], ids: [..], clients } */ + groupSsids: function(ssids) { + var out = [], idx = Object.create(null); + arr(ssids).forEach(function(s) { + var k = s.ssid + '\u0000' + s.security.short; + if (!idx[k]) { idx[k] = { key: k, ssid: s.ssid, security: s.security, pmf: s.pmf, hidden: s.hidden, bands: [], ids: [], radios: [], clients: 0, up: false }; out.push(idx[k]); } + var g = idx[k]; + if (s.band && g.bands.indexOf(s.band) < 0) g.bands.push(s.band); + if (g.radios.indexOf(s.radio) < 0) g.radios.push(s.radio); + g.ids.push(s.id); + g.clients += s.clients; + g.up = g.up || s.up; + }); + return out; + } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/names.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/names.js new file mode 100644 index 000000000..45709493d --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/names.js @@ -0,0 +1,282 @@ +'use strict'; +'require baseclass'; +'require vantage.fmt as fmt'; + +/* Client naming. An access point that is not the DHCP server knows almost + nothing about its stations: no hostnames, often not even an address, and + most phones use randomised ("private") MAC addresses. This module turns + whatever is available into one human label plus the source it came + from, in a fixed order of trust: + + alias the user's own name (uci vantage, section type client) + dns reverse DNS of the station's address (network.rrdns) + dhcp a hostname in LuCI's host hints (DHCP lease on routers); + keyed by the station's own MAC + mdns mDNS host announcement (umdns hosts). Unauthenticated, and + umdns does not say who sent it, so any host on the LAN can + claim any address: used only below DNS and DHCP, and only + when uncontested (one name for the address, one station for + the name; see mdnsMap and mdnsForStations) + wps the device name the station sends in its WPS element + vendor manufacturer from the OUI of a globally unique MAC + private "Private device" for a locally administered MAC + mac "Device" as the last resort + + Pure functions; names are untrusted and only ever rendered as text. */ + +var NAME_MAX = 48; +/* client sections read from /etc/config/vantage; the rpcd plugin refuses + to store more */ +var ALIAS_MAX = 512; + +/* C0 (incl. tab, CR, LF), DEL and C1 controls */ +var CONTROL = /[\u0000-\u001f\u007f-\u009f]/; +/* a high surrogate not followed by a low one, or a low one not preceded + by a high one */ +var LONE_SURROGATE = /[\ud800-\udbff](?![\udc00-\udfff])|(^|[^\ud800-\udbff])[\udc00-\udfff]/; +/* space characters that collapse to one U+0020 (controls are rejected + before this applies) */ +var SPACES = /[\u0020\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]+/g; + +var SOURCES = { + alias: _('Alias'), dns: _('DNS'), mdns: _('mDNS'), dhcp: _('DHCP'), wps: _('WPS'), + vendor: _('Vendor'), 'private': _('Private'), mac: _('MAC') +}; + +/* device icons the user can pick; also guessed from names */ +var ICONS = [ 'phone', 'laptop', 'tablet', 'desktop', 'tv', 'speaker', 'printer', 'camera', 'console', 'watch', 'iot', 'router', 'device' ]; + +var GUESS = [ + [ /\b(print|printer|envy|officejet|deskjet|laserjet|pixma|ecotank|mfc-|hl-|brother|epson|canon)/i, 'printer' ], + [ /\b(iphone|pixel|galaxy|android|phone|redmi|oneplus|xperia|moto)/i, 'phone' ], + [ /\b(ipad|tablet|tab\b|kindle|fire hd)/i, 'tablet' ], + [ /\b(macbook|laptop|notebook|thinkpad|surface|chromebook|xps|zenbook|ideapad)/i, 'laptop' ], + [ /\b(imac|desktop|workstation|\bpc\b|mac mini|nuc)/i, 'desktop' ], + [ /\b(tv|bravia|roku|chromecast|fire ?tv|shield|apple ?tv|webos|tizen)/i, 'tv' ], + [ /\b(echo|sonos|homepod|speaker|nest mini|google home)/i, 'speaker' ], + [ /\b(cam|camera|doorbell|ring|arlo|wyze|eufy)/i, 'camera' ], + [ /\b(playstation|ps[345]|xbox|nintendo|switch|steam ?deck)/i, 'console' ], + [ /\b(watch)/i, 'watch' ], + [ /\b(esp[-_]?\d|shelly|tasmota|sonoff|tuya|hue|plug|bulb|thermostat|sensor)/i, 'iot' ], + [ /\b(router|access point|\bap\b|openwrt|fritz|repeater|mesh)/i, 'router' ] +]; + +function str(v) { return typeof v === 'string' ? v : ''; } + +return baseclass.extend({ + NAME_MAX: NAME_MAX, + ALIAS_MAX: ALIAS_MAX, + SOURCES: SOURCES, + ICONS: ICONS, + + /* '02-00-5E-00-53-0F' / '0200.5e00.530f' / '02005e00530f' -> + '02:00:5e:00:53:0f' (one separator throughout), else null */ + normMac: function(mac) { + var hex = str(mac).trim().toLowerCase(); + if (/^[0-9a-f]{2}([:-])(?:[0-9a-f]{2}\1){4}[0-9a-f]{2}$/.test(hex)) return hex.replace(/-/g, ':'); + if (/^[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4}$/.test(hex) || /^[0-9a-f]{12}$/.test(hex)) { + hex = hex.replace(/\./g, ''); + return hex.match(/../g).join(':'); + } + return null; + }, + + /* locally administered (U/L bit set): randomised / private address */ + isPrivateMac: function(mac) { + var m = this.normMac(mac); + return !!m && (parseInt(m.slice(0, 2), 16) & 2) === 2; + }, + + /* group bit set: not a station address */ + isMulticastMac: function(mac) { + var m = this.normMac(mac); + return !!m && (parseInt(m.slice(0, 2), 16) & 1) === 1; + }, + + /* 'AA:BB:CC' key for the vendor table, or null for private/multicast */ + ouiKey: function(mac) { + var m = this.normMac(mac); + if (!m || this.isPrivateMac(m) || this.isMulticastMac(m)) return null; + return m.slice(0, 8).toUpperCase(); + }, + + /* hostapd client signature "wifi4|probe:...,wps:Model_Name|assoc:..." + -> 'Model Name' (hostapd replaces spaces with '_') */ + wpsName: function(signature) { + var m = str(signature).match(/(?:^|[|,:])wps:([^|,]{1,64})/); + if (!m) return null; + var name = m[1].replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); + return /[A-Za-z0-9]/.test(name) ? fmt.safeText(name, NAME_MAX) : null; + }, + + /* 'phone.lan.' -> 'phone'; keeps names that are not below a local + search domain as they are */ + cleanHostname: function(name) { + var n = str(name).trim().replace(/\.$/, ''); + if (!n || /^[\d.]+$/.test(n) || /:/.test(n)) return null; /* an address, not a name */ + n = n.replace(/\.(lan|local|home|localdomain|home\.arpa|internal|fritz\.box|station)$/i, ''); + return n ? fmt.safeText(n, NAME_MAX) : null; + }, + + /* User alias from the inspector. Rejected: anything that is not a + string, control characters (C0 including tab and newline, DEL, C1), + invisible/bidi/format characters (fmt.safeText's set) and unpaired + surrogates; these are checked before any normalisation so nothing + is silently turned into a space. Then runs of spaces (U+0020 and the + Unicode space separators) collapse to one space, the ends are + trimmed, and at most NAME_MAX code points are allowed. + -> { ok, value } or { ok: false, error } ; '' clears. */ + validateAlias: function(name) { + if (name == null) name = ''; + if (typeof name !== 'string') return { ok: false, error: _('Name must be text') }; + if (name.length > NAME_MAX * 8) return { ok: false, error: _('Name is longer than %d characters').format(NAME_MAX) }; + if (CONTROL.test(name) || LONE_SURROGATE.test(name) || fmt.safeText(name, name.length + 2) !== name) + return { ok: false, error: _('Name contains invisible or control characters') }; + var s = name.replace(SPACES, ' ').trim(); + if (!s) return { ok: true, value: '' }; + if (Array.from(s).length > NAME_MAX) return { ok: false, error: _('Name is longer than %d characters').format(NAME_MAX) }; + return { ok: true, value: s }; + }, + + /* The uci change that saves a station name, re-validating everything + whatever the form did: the MAC must normalise to a unicast station + address, the name must pass validateAlias, an icon outside ICONS is + dropped, and an existing section id must be a plain uci identifier. + existing: the aliasMap entry for this MAC, or null. + -> { ok: true, op: 'none' | 'add' | 'set' | 'delete', sid, values } + | { ok: false, error } */ + aliasOp: function(existing, mac, name, icon) { + var m = this.normMac(mac); + if (!m || this.isMulticastMac(m)) return { ok: false, error: _('Not a station MAC address') }; + var v = this.validateAlias(name); + if (!v.ok) return v; + var sid = null; + if (existing) { + sid = typeof existing.sid === 'string' && /^[A-Za-z0-9_]{1,64}$/.test(existing.sid) ? existing.sid : null; + if (!sid) return { ok: false, error: _('The stored name has an invalid section id') }; + } + var ic = this.validIcon(icon); + if (!v.value) return sid ? { ok: true, op: 'delete', sid: sid } : { ok: true, op: 'none' }; + if (sid) return { ok: true, op: 'set', sid: sid, values: { name: v.value, icon: ic || '' } }; + var values = { mac: m, name: v.value }; + if (ic) values.icon = ic; + return { ok: true, op: 'add', values: values }; + }, + + validIcon: function(icon) { return ICONS.indexOf(icon) >= 0 ? icon : null; }, + + guessIcon: function(name) { + var n = str(name); + for (var i = 0; i < GUESS.length; i++) if (GUESS[i][0].test(n)) return GUESS[i][1]; + return null; + }, + + /* uci vantage -> { 'aa:bb:..': { sid, name, icon } }; invalid MACs and + empty names are skipped, the first section for a MAC wins, sections + past ALIAS_MAX are ignored */ + aliasMap: function(sections) { + var out = Object.create(null), self = this, n = 0; + (Array.isArray(sections) ? sections : []).forEach(function(s) { + if (!s || s['.type'] !== 'client' || ++n > ALIAS_MAX) return; + var mac = self.normMac(s.mac), v = self.validateAlias(s.name); + if (!mac || out[mac] || !v.ok || !v.value) return; + out[mac] = { sid: str(s['.name']), name: v.value, icon: self.validIcon(s.icon) }; + }); + return out; + }, + + /* umdns hosts reply { 'name.local': { ipv4: '...', ipv6: '...' } } + -> { ip: name }. An address claimed by two different names is left + out: umdns caches unsolicited answers from anyone, so a second claim + is either a conflict or a spoof, and neither names the station. */ + mdnsMap: function(hosts) { + var out = Object.create(null), bad = Object.create(null), self = this; + if (!hosts || typeof hosts !== 'object') return out; + Object.keys(hosts).forEach(function(host) { + var h = hosts[host], name = self.cleanHostname(host); + if (!name || !h || typeof h !== 'object') return; + [ 'ipv4', 'ipv6' ].forEach(function(k) { + var v = h[k]; + (Array.isArray(v) ? v : [ v ]).forEach(function(ip) { + if (typeof ip !== 'string' || !ip || bad[ip]) return; + if (!out[ip]) out[ip] = name; + else if (out[ip] !== name) { delete out[ip]; bad[ip] = true; } + }); + }); + }); + return out; + }, + + /* The part of an mdnsMap that may name these stations + ([ { mac, ips } ]): an address that belongs to more than one station + names none of them, and a name that would land on more than one + station is dropped for all of them, so one announcement cannot label + a second device. -> { ip: name } */ + mdnsForStations: function(map, stations) { + var owners = Object.create(null), byName = Object.create(null), out = Object.create(null), self = this; + if (!map) return out; + (Array.isArray(stations) ? stations : []).forEach(function(s) { + var mac = s && self.normMac(s.mac); + if (!mac) return; + (Array.isArray(s.ips) ? s.ips : []).forEach(function(ip) { + if (typeof ip !== 'string') return; + var o = owners[ip] || (owners[ip] = []); + if (o.indexOf(mac) < 0) o.push(mac); + }); + }); + Object.keys(owners).forEach(function(ip) { + if (owners[ip].length !== 1 || !Object.prototype.hasOwnProperty.call(map, ip)) return; + var name = map[ip], macs = byName[name] || (byName[name] = []); + if (macs.indexOf(owners[ip][0]) < 0) macs.push(owners[ip][0]); + }); + Object.keys(owners).forEach(function(ip) { + if (owners[ip].length === 1 && Object.prototype.hasOwnProperty.call(map, ip) && byName[map[ip]].length === 1) out[ip] = map[ip]; + }); + return out; + }, + + /* Resolve the display name of one station. + s: { mac, alias: { name, icon }, ips: [..], rdns: { ip: name }, + mdns: { ip: name }, hint: { name }, wps, vendor } + -> { name, source, sourceLabel, icon, secondary, isPrivate } */ + resolve: function(s) { + s = s || {}; + var self = this, mac = this.normMac(s.mac) || str(s.mac); + var ips = Array.isArray(s.ips) ? s.ips.filter(function(ip) { return typeof ip === 'string' && ip; }) : []; + var priv = this.isPrivateMac(mac), name = null, source = null; + + function fromIps(map) { + if (!map) return null; + for (var i = 0; i < ips.length; i++) { + var n = Object.prototype.hasOwnProperty.call(map, ips[i]) ? self.cleanHostname(map[ips[i]]) : null; + if (n) return n; + } + return null; + } + + var alias = s.alias && this.validateAlias(s.alias.name); + if (alias && alias.ok && alias.value) { name = alias.value; source = 'alias'; } + if (!name && (name = fromIps(s.rdns))) source = 'dns'; + if (!name && s.hint && (name = this.cleanHostname(s.hint.name))) source = 'dhcp'; + if (!name && (name = fromIps(s.mdns))) source = 'mdns'; + if (!name && s.wps && (name = fmt.safeText(str(s.wps).trim(), NAME_MAX) || null)) source = 'wps'; + + var tail = fmt.macTail(mac); + if (!name) { + if (priv) { name = _('Private device') + ' · ' + tail; source = 'private'; } + else if (s.vendor) { name = _('%s device').format(fmt.safeText(s.vendor, 32)) + ' · ' + tail; source = 'vendor'; } + else { name = _('Device') + ' · ' + tail; source = 'mac'; } + } + + var icon = (s.alias && this.validIcon(s.alias.icon)) || this.guessIcon(source === 'vendor' ? s.vendor : name) || 'device'; + return { + name: name, + source: source, + sourceLabel: SOURCES[source], + icon: icon, + /* second line: the address when there is one, else the MAC */ + secondary: ips.length ? ips[0] : fmt.mac(mac), + isPrivate: priv + }; + } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/oui.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/oui.js new file mode 100644 index 000000000..a8486e0f9 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/oui.js @@ -0,0 +1,84 @@ +'use strict'; +'require baseclass'; + +/* Compact manufacturer table (OUI -> vendor) for station naming. + + Loaded lazily by the dashboard, only when a station has a globally + unique MAC and no better name. Deliberately small and best-effort: a + few hundred prefixes of vendors common on home and small-office + networks, written from the public IEEE registry. Unknown prefixes just + fall back to "Device". A hit is shown with the source tag "Vendor", so + it reads as a hint, not a fact. + + Grouped per vendor as space-separated 6-hex-digit prefixes. */ + +var TABLE = { + 'Apple': '000393 000502 000A27 000A95 000D93 0010FA 001124 001451 0016CB 0017F2 0019E3 001B63 001CB3 001D4F 001E52 001EC2 001F5B 001FF3 0021E9 002241 002312 002332 00236C 0023DF 002436 002500 00254B 0025BC 002608 00264A 0026B0 0026BB 003065 0050E4 0056CD 006171 008865 00B362 00C610 00CDFE 00DB70 00F4B9 00F76F 040CCE 041552 041E64 042665 04489A 044BED 0452F3 045453 0469F8 04D3CF 04DB56 04E536 04F13E 04F7E4 086698 086D41 087045 0C74C2 1040F3 10417F 1093E9 10DDB1 14109F 145A05 148FC6 182032 183451 186590 18AF61 18E7F4 1C1AC0 1CABA7 2078F0 20A2E4 24A074 24A2E1 283737 286ABA 28CFDA 28CFE9 28E02C 28E7CF 2CBE08 3010E4 34159E 34C059 38C986 3C0754 3C15C2 403004 406C8F 40A6D9 442A60 48437C 4C8D79 50EAD6 542696 54724F 5855CA 5C5948 5CF938 60334B 60C547 60FB42 64200C 64B9E8 68A86D 6C4008 705681 70CD60 74E1B6 7831C1 78CA39 7C6D62 7CC3A1 80929F 843835 848506 881FA1 885395 8C2DAA 8C5877 9027E4 907240 9803D8 98FE94 9C207B A45E60 A4B197 A82066 A886DD AC87A3 B03495 B817C2 B8C75D B8F6B1 BC52B7 C0847A C82A14 C8BCC8 CC08E0 D023DB D49A20 D83062 D8A25E DC2B61 E0F847 E4CE8F E80688 F0B479 F41BA1 F81EDF', + 'Samsung': '0000F0 0007AB 001247 0012FB 001377 001599 001632 00166B 00166C 0017C9 0017D5 0018AF 001A8A 001B98 001C43 001D25 001E7D 001FCC 002119 002339 002454 002490 002637 08D42B 101DC0 1449E0 183A2D 1C62B8 244B81 28987B 30074D 3423BA 38AA3C 400E85 5001BB 5C0A5B 78471D 8425DB 8C7712 94350A A00BBA BC20A4 C4576E D022BE E4121D F025B7 F47B5E FCA13E', + 'Google': '001A11 1CF29A 3C5AB4 48D6D5 546009 94EB2C A47733 D86C63 F4F5D8 F4F5E8', + 'Amazon': '00FC8B 0C47C9 10CEA9 34D270 38F73D 40B4CD 44650D 50DCE7 6837E9 6854FD 74C246 84D6D0 8871E5 A002DC AC63BE B47C9C F0272D FC65DE FCA183', + 'Raspberry Pi': 'B827EB DCA632 E45F01 D83ADD 28CDC1 2CCF67', + 'Espressif': '240AC4 2462AB 246F28 30AEA4 3C71BF 4C11AE 5CCF7F 600194 807D3A 840D8E 84F3EB 8CAAB5 A020A6 A4CF12 AC67B2 B4E62D BCDDC2 C44F33 CC50E3 D8A01D DC4F22 ECFABC 18FE34 68C63A 98F4AB 7C9EBD 94B97E E8DB84 34865D 483FDA 10521C 083AF2 C82B96 40F520 58BF25 3C6105 308398', + 'Intel': '0002B3 000347 000423 0007E9 000CF1 000E0C 000E35 001111 0012F0 001302 001320 0013CE 0013E8 001500 00166F 001676 0016EA 0016EB 0018DE 0019D1 0019D2 001B21 001B77 001CBF 001CC0 001DE0 001DE1 001E64 001E65 001E67 001F3B 001F3C 00215C 00215D 00216A 00216B 0022FA 0022FB 002314 002315 0024D6 0024D7 0026C6 0026C7 002710 3CA9F4 4C3488 605718 6805CA 7C5CF8 8086F2 8CA982 94659C A08869 A44E31 AC7289 B46BFC DC5360 E8B1FC F81654 3413E8 3C6AA7 4851B7 5C879C 701CE7 843A4B 984FEE A0A8CD C85EA9 D0577B 04D3B0 001517', + 'HP': '0001E6 0001E7 0002A5 0004EA 000802 000BCD 000D9D 000E7F 000F20 000F61 001083 00110A 001185 001279 001321 001438 0014C2 001560 001635 001708 0017A4 001871 0018FE 0019BB 001A4B 001B78 001CC4 001E0B 001F29 00215A 002264 00237D 002481 0025B3 002655 00306E 0060B0 101F74 10604B 1CC1DE 2C4138 2C44FD 308D99 3863BB 3CD92B 5820B1 645106 6CC217 705A0F 78ACC0 80C16E 9457A5 98E7F4 A01D48 A0D3C1 B499BA C8CBB8 D4C9EF E4115B ECB1D7 F4CE46 FC15B4', + 'Xiaomi': '009EC8 0C1DAF 102AB3 14F65A 185936 2082C0 286C07 3480B3 38A4ED 3CBD3E 4C49E3 508F4C 584498 640980 64B473 68DFDD 742344 7802F8 7C1DD9 8CBEBE 98FAE3 9C99A0 A086C6 ACC1EE B0E235 C40BCB C46AB7 D4970B F0B429 F48B32 FC64BA 50EC50 7C49EB', + 'Huawei': '001882 001E10 002568 00259E 00464B 00664B 00E0FC 04C06F 0819A6 0C37DC 101B54 20F3A3 283152 30D17E 404D8E 4846FB 4C5499 548998 5C7D5E 70723C 78D752 80B686 8853D4 8C34FD ACE215 BC7670 C8D15E D07AB5 E0247F F4C714', + 'TP-Link': '001D0F 0023CD 002586 002719 10FEED 14CC20 14CF92 18A6F7 1C3BF3 246968 30B5C2 3460F9 50C7BF 54C80F 5C628B 60E327 6466B3 647002 6C5AB0 74DA88 788CB5 90F652 98DAC4 A0F3C1 AC84C6 B04E26 B09575 C025E9 C46E1F C83A35 D807B6 E4C32A E848B8 EC086B F4F26D F81A67 503EAA 984827 8416F9 1C61B4 3C846A 9CA2F4 B0BE76 2887BA 5CA6E6 6032B1', + 'Netgear': '00095B 000FB5 00146C 00184D 001B2F 001E2A 001F33 00223F 0024B2 0026F2 04A151 08BD43 100D7F 204E7F 28C68E 2CB05D 30469A 4494FC 6CB0CE 841B5E 9C3DCF A00460 A021B7 B07FB9 C03F0E C40415 E0469A E091F5', + 'Ubiquiti': '00156D 002722 0418D6 18E829 24A43C 44D9E7 687251 7483C2 788A20 802AA8 B4FBE4 DC9FDB E063DA F09FC2 FCECDA 245A4C 602232 70A741 942A6F D021F9 E43883', + 'Zyxel': '001349 0019CB 0023F8 00A0C5 107BEF 404A03 5CF4AB 588BF3 B8ECA3 BC9911 C86C87 CC5D4E E4186B EC43F6 FCF528 90EF68', + 'AVM': '00040E 246511 3810D5 3CA62F 444E6D 5C4979 7CFF4D 989BCB B0F208 C02506 CCCE1E DC396F E0286D F0B014 2C91AB 3431C4 74427F', + 'ASUS': '000C6E 000EA6 00112F 0011D8 0013D4 0015F2 001731 0018F3 001A92 001BFC 001D60 001E8C 001FC6 002215 002354 00248C 002618 049226 04D4C4 08606E 10BF48 14DAE9 1C872C 2C4D54 2C56DC 305A3A 382C4A 40167E 50465D 5404A6 6045CB 74D02B 88D7F6 AC220B AC9E17 B06EBF BCEE7B C86000 D850E6 E03F49 F07959 F832E4 04421A 244BFE 7C10C9 A85E45', + 'Sony': '00041F 001315 0015C1 0019C5 001D0D 001FA7 00248D 00D9D1 280DFC 709E29 A8E3EE BC60A7 F8461C FC0FE6 00EB2D 78C881 2CCC44', + 'Nintendo': '0009BF 001656 0017AB 00191D 0019FD 001AE9 001B7A 001BEA 001CBE 001DBC 001E35 001EA9 001F32 001FC5 002147 0021BD 00224C 0022AA 0022D7 002331 0023CC 00241E 002444 0024F3 0025A0 002659 002709 0403D6 2C10C1 34AF2C 40F407 58BDA3 7CBB8A 8C56C5 98B6E9 9CE635 A4C0E1 B8AE6E CC9E00 D86BF7 DC68EB E00C7F E84ECE 64B5C6', + 'Microsoft': '000D3A 00125A 00155D 0017FA 001DD8 002248 0025AE 281878 3059B7 501AC5 6045BD 7C1E52 7CED8D 985FD3 B4AE2B C83F26 DCB4C4 5882A8', + 'Sonos': '000E58 347E5C 48A6B8 542A1B 5CAAFD 7828CA 949F3E B8E937 F0F6C1 38420B', + 'Roku': '000D4B 080581 105932 20EFBD B0A737 B83E59 C83A6B D04D2C DC3A5E AC3A7A CC6DA0', + 'LG': '001C62 001E75 001F6B 001FE3 0021FB 0022A9 002483 0025E5 0026E2 00AA70 10683F 203DBD 2C54CF 344DF7 388C50 40B0FA 58A2B5 64995D 6CD68A 785DC8 88C9D0 A816B2 BCF5AC C49A02 CC2D8C E85B5B F80CF3', + 'Dell': '00065B 000874 000BDB 000D56 000F1F 001143 00123F 001372 001422 0015C5 00188B 0019B9 001AA0 001C23 001D09 001E4F 002170 00219B 002219 0023AE 0024E8 002564 0026B9 141877 14FEB5 180373 1866DA 18A99B 18DBF2 204747 24B6FD 28F10E 3417EB 34E6D7 44A842 484D7E 4C7625 509A4C 549F35 5C260A 74867A 782BCB 847BEB 90B11C 989096 A41F72 B083FE B82A72 B8AC6F BC305B C81F66 D067E5 D481D7 D4AE52 D4BED9 E0DB55 ECF4BB F01FAF F48E38 F8B156 F8BC12 F8DB88', + 'Signify (Hue)': '001788 ECB5FA', + 'Google Nest': '18B430 641666', + 'Belkin': '94103E EC1A59 08863B 149182', + 'Canon': '000085 001E8F 180CAC 2C9EFC F48139 888717 74BFC0', + 'Epson': '000048 0026AB 64EB8C A4EE57 44D244 381A52 9CAED3 AC1826', + 'Brother': '001BA9 008077 30055C 3C2AF4', + 'OnePlus': '94652D C0EEFB 64A2F9', + 'Realtek': '00E04C', + 'Qualcomm': '00037F 00A0C6', + 'MediaTek': '000CE7 000C43', + 'Murata': '000E6D 0026E8 00376D 1C994C 40F308 44A7CF 5CDAD4 6021C0 88308A 90B686 98F170 F02765 FCC2DE', + 'AzureWave': '0015AF 0025D3 1C4BD6 24FD52 485D60 54271E 6C71D9 74C63B 80A589 94DBC9 B0EE45 D0C5D3 DC85DE E0B9A5', + 'Synology': '001132', + 'QNAP': '00089B 245EBE', + 'Hikvision': '4419B6 BCAD28 C056E3 4CBD8F', + 'Dahua': '3CEF8C 9002A9 E0508B', + 'MikroTik': '000C42 4C5E0C 64D154 6C3B6B 744D28 B869F4 CC2DE0 D4CA6D DC2C6E E48D8C 18FD74 2CC81B 488F5A 085531', + 'Linksys': '00045A 000625 000C41 000F66 001217 001310 0014BF 0016B6 001839 0018F8 001A70 001C10 001D7E 001EE5 002129 00226B 002369 00259C 20AA4B 48F8B3 586D8F 687F74 C0C1C0 C8B373', + 'D-Link': '00055D 000D88 000F3D 001195 001346 0015E9 00179A 00195B 001B11 001CF0 001E58 002191 0022B0 002401 00265A 14D64D 1C7EE5 28107B 340804 5CD998 78542E 84C9B2 9094E4 B8A386 C0A0BB C4A81D C8BE19 CCB255 F07D68 FC7516', + 'Cisco': '00000C 000142 001BD4 004096', + 'Wyze': '2CAA8E 7C78B2', + 'Tesla': '4CFCAA' +}; + +var map = null; + +function build() { + map = new Map(); + Object.keys(TABLE).forEach(function(vendor) { + TABLE[vendor].split(' ').forEach(function(p) { + if (/^[0-9A-F]{6}$/.test(p) && !map.has(p)) map.set(p, vendor); + }); + }); + return map; +} + +return baseclass.extend({ + /* 'AA:BB:CC' (names.ouiKey) or a full MAC -> vendor name or null */ + lookup: function(key) { + var hex = String(key == null ? '' : key).replace(/[^0-9A-Fa-f]/g, '').slice(0, 6).toUpperCase(); + if (hex.length !== 6) return null; + return (map || build()).get(hex) || null; + }, + + size: function() { return (map || build()).size; } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/series.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/series.js new file mode 100644 index 000000000..01ed7d7ed --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/series.js @@ -0,0 +1,136 @@ +'use strict'; +'require baseclass'; + +/* Time series kept in the browser for the session, and the SVG path + geometry to draw them. A series is an array of [ t (ms), v0, v1, ... ] + in strictly increasing t. Pure functions. */ + +function finite(v) { return typeof v === 'number' && isFinite(v); } +function r1(v) { return Math.round(v * 10) / 10; } + +return baseclass.extend({ + /* append, keeping entries newer than now - windowMs and at most max */ + push: function(series, point, windowMs, max) { + var out = Array.isArray(series) ? series : []; + if (!Array.isArray(point) || !finite(point[0])) return out; + if (out.length && out[out.length - 1][0] >= point[0]) return out; + out.push(point); + var cutoff = point[0] - (windowMs > 0 ? windowMs : Infinity); + while (out.length && (out[0][0] < cutoff || (max > 0 && out.length > max))) out.shift(); + return out; + }, + + /* Stored series (sessionStorage) are untrusted: keep arrays of exactly + `width` finite numbers with values >= 0 (except t), strictly + increasing t, nothing after notAfter, at most maxLen (newest). */ + valid: function(arr, width, maxLen, notAfter) { + if (!Array.isArray(arr)) return []; + var out = [], lastT = -Infinity; + for (var i = 0; i < arr.length; i++) { + var p = arr[i], ok = Array.isArray(p) && p.length === width; + for (var j = 0; ok && j < width; j++) ok = finite(p[j]) && (j === 0 || p[j] >= 0); + if (!ok || p[0] <= lastT || (notAfter != null && p[0] > notAfter)) continue; + out.push(p.slice()); + lastT = p[0]; + } + return out.length > maxLen ? out.slice(out.length - maxLen) : out; + }, + + /* a round axis maximum >= v: 1, 2, 2.5, 5 x 10^n */ + niceMax: function(v) { + if (!finite(v) || v <= 0) return 1; + var e = Math.pow(10, Math.floor(Math.log10(v))), f = v / e; + var n = f <= 1 ? 1 : f <= 2 ? 2 : f <= 2.5 ? 2.5 : f <= 5 ? 5 : 10; + return n * e; + }, + + /* a round axis maximum >= v with finer steps (1, 1.5, 2, 2.5, 3, 4, + 5, 6, 8, 10 x 10^n), so a 374 Mbit/s peak gets a 400 axis, not 500 */ + niceCeil: function(v) { + if (!finite(v) || v <= 0) return 1; + var e = Math.pow(10, Math.floor(Math.log10(v))), f = v / e; + var steps = [ 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10 ]; + for (var i = 0; i < steps.length; i++) + if (f <= steps[i] * (1 + 1e-9)) return steps[i] * e; + return 10 * e; + }, + + /* { now, peak, avg, n } of column idx over points with t >= t0 (now: + the newest finite value); nulls when there is no point */ + stats: function(series, idx, t0) { + var n = 0, sum = 0, peak = null, now = null; + (series || []).forEach(function(p) { + if (!Array.isArray(p) || !finite(p[idx]) || (t0 != null && p[0] < t0)) return; + n++; sum += p[idx]; + if (peak == null || p[idx] > peak) peak = p[idx]; + now = p[idx]; + }); + return { now: now, peak: peak, avg: n ? sum / n : null, n: n }; + }, + + /* Time domain of a chart that fills in while history accumulates: + [t1 - span, t1] where span is the age of the oldest point (of any + of the series) clamped to [minMs, maxMs]. age: of the oldest point; + full: the whole window is covered. -> { t0, t1, span, age, full } */ + domain: function(list, t1, minMs, maxMs) { + var first = Infinity; + (list || []).forEach(function(sr) { + (sr || []).forEach(function(p) { if (Array.isArray(p) && finite(p[0]) && p[0] < first) first = p[0]; }); + }); + var span = isFinite(first) ? Math.max(minMs, Math.min(maxMs, t1 - first)) : minMs; + return { t0: t1 - span, t1: t1, span: span, age: isFinite(first) ? Math.min(maxMs, t1 - first) : 0, full: span >= maxMs - 1000 }; + }, + + /* max over columns idx of the series (for a shared y scale) */ + max: function(series, idx) { + var m = 0; + (series || []).forEach(function(p) { + idx.forEach(function(i) { if (finite(p[i]) && p[i] > m) m = p[i]; }); + }); + return m; + }, + + /* Screen points of column idx within a w x h box: x by time over + [t0, t1], y by value over [0, vmax] (0 at the bottom). A gap longer + than maxGap ms starts a new segment. -> [ [ [x, y], ... ], ... ] */ + segments: function(series, idx, t0, t1, vmax, w, h, maxGap) { + var segs = [], cur = null, lastT = null, span = (t1 - t0) || 1; + vmax = vmax > 0 ? vmax : 1; + (series || []).forEach(function(p) { + var v = p[idx]; + if (!finite(v) || p[0] < t0 - (maxGap || 0)) { cur = null; return; } + if (cur && maxGap && lastT != null && p[0] - lastT > maxGap) cur = null; + if (!cur) { cur = []; segs.push(cur); } + var x = (p[0] - t0) / span * w, y = h - Math.min(1, Math.max(0, v / vmax)) * h; + cur.push([ r1(x), r1(y) ]); + lastT = p[0]; + }); + return segs; + }, + + /* 'M x y L x y ...' for line segments */ + linePath: function(segs) { + return (segs || []).filter(function(s) { return s.length > 0; }).map(function(s) { + if (s.length === 1) return 'M' + s[0][0] + ' ' + s[0][1] + 'h0.1'; + return 'M' + s.map(function(pt) { return pt[0] + ' ' + pt[1]; }).join('L'); + }).join(''); + }, + + /* closed area down to the baseline y = h */ + areaPath: function(segs, h) { + return (segs || []).filter(function(s) { return s.length > 1; }).map(function(s) { + return 'M' + s[0][0] + ' ' + h + 'L' + s.map(function(pt) { return pt[0] + ' ' + pt[1]; }).join('L') + + 'L' + s[s.length - 1][0] + ' ' + h + 'Z'; + }).join(''); + }, + + /* index of the point closest in time to t (for hover), or -1 */ + nearest: function(series, t) { + var best = -1, bd = Infinity; + (series || []).forEach(function(p, i) { + var d = Math.abs(p[0] - t); + if (d < bd) { bd = d; best = i; } + }); + return best; + } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js b/luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js new file mode 100644 index 000000000..1db20f90d --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js @@ -0,0 +1,161 @@ +'use strict'; +'require baseclass'; +'require vantage.fmt as fmt'; + +/* Wi-Fi vocabulary: bands, channel widths, generations, signal levels, + security labels and channel geometry. Pure functions. */ + +function num(v) { v = +v; return isFinite(v) ? v : 0; } +function own(o, k) { return o != null && Object.prototype.hasOwnProperty.call(o, k); } + +var BANDS = { + '2g': { label: _('2.4 GHz'), short: '2.4', lo: 2400, hi: 2495, base: 2407 }, + '5g': { label: _('5 GHz'), short: '5', lo: 5150, hi: 5895, base: 5000 }, + '6g': { label: _('6 GHz'), short: '6', lo: 5925, hi: 7125, base: 5950 }, + '60g': { label: _('60 GHz'), short: '60', lo: 57240, hi: 70200, base: 56160 } +}; + +/* rank for comparisons; 'legacy' is 802.11a/b/g */ +var GEN_RANK = { 'legacy': 0, 'Wi-Fi 4': 4, 'Wi-Fi 5': 5, 'Wi-Fi 6': 6, 'Wi-Fi 6E': 6.5, 'Wi-Fi 7': 7 }; + +/* UCI encryption -> friendly label. Suffixes like +ccmp are cipher + choices, not a different protocol. */ +var SECURITY = [ + [ /^none$/, _('Open'), _('Open network'), 'open' ], + [ /^owe$/, _('OWE'), _('Enhanced Open (OWE)'), 'owe' ], + [ /^sae-mixed$/, _('WPA2/3'), _('WPA2/WPA3-Personal'), 'ok' ], + [ /^sae(-ext)?$/, _('WPA3'), _('WPA3-Personal'), 'ok' ], + [ /^psk-mixed$/, _('WPA/WPA2'), _('WPA/WPA2-Personal (TKIP allowed)'), 'weak' ], + [ /^psk2$/, _('WPA2'), _('WPA2-Personal'), 'ok' ], + [ /^psk$/, _('WPA'), _('WPA-Personal (legacy)'), 'weak' ], + [ /^wpa3-mixed$/, _('WPA2/3-Ent'), _('WPA2/WPA3-Enterprise'), 'ok' ], + [ /^wpa3(-192)?$/, _('WPA3-Ent'), _('WPA3-Enterprise'), 'ok' ], + [ /^wpa-mixed$/, _('WPA/WPA2-Ent'), _('WPA/WPA2-Enterprise'), 'weak' ], + [ /^wpa2$/, _('WPA2-Ent'), _('WPA2-Enterprise'), 'ok' ], + [ /^wpa$/, _('WPA-Ent'), _('WPA-Enterprise (legacy)'), 'weak' ], + [ /^wep/, _('WEP'), _('WEP (insecure)'), 'weak' ] +]; + +return baseclass.extend({ + BANDS: BANDS, + + /* '2g' / frequency (MHz) -> '2g' | '5g' | '6g' | '60g' | null */ + bandKey: function(cfgBand, freq) { + if (typeof cfgBand === 'string' && own(BANDS, cfgBand)) return cfgBand; + freq = num(freq); + if (!freq) return null; + return freq < 2500 ? '2g' : freq < 5925 ? '5g' : freq < 7200 ? '6g' : '60g'; + }, + + bandLabel: function(key) { + return own(BANDS, key) ? BANDS[key].label : fmt.DASH; + }, + + /* 'EHT160' -> { std: 'EHT', gen: 'Wi-Fi 7', width: 160 } */ + htmode: function(mode) { + var m = String(mode || '').toUpperCase().match(/^(EHT|HE|VHT|HT|NOHT)(\d+)?/); + if (!m) return { std: null, gen: null, width: null }; + var gen = { EHT: 'Wi-Fi 7', HE: 'Wi-Fi 6', VHT: 'Wi-Fi 5', HT: 'Wi-Fi 4', NOHT: 'legacy' }[m[1]]; + return { std: m[1] === 'NOHT' ? null : m[1], gen: gen, width: m[2] ? +m[2] : (m[1] === 'NOHT' ? 20 : null) }; + }, + + /* Wi-Fi generation, marketing name. HE on 6 GHz is "Wi-Fi 6E". */ + genName: function(std, band) { + switch (std) { + case 'EHT': return 'Wi-Fi 7'; + case 'HE': return band === '6g' ? 'Wi-Fi 6E' : 'Wi-Fi 6'; + case 'VHT': return 'Wi-Fi 5'; + case 'HT': return 'Wi-Fi 4'; + case 'legacy': return 'legacy'; + } + return null; + }, + + genRank: function(gen) { return own(GEN_RANK, gen) ? GEN_RANK[gen] : -1; }, + + /* std of one iwinfo rate block ({ ht, vht, he, eht, ... }) */ + rateStd: function(r) { + if (!r || typeof r !== 'object') return null; + return r.eht ? 'EHT' : r.he ? 'HE' : r.vht ? 'VHT' : r.ht ? 'HT' : 'legacy'; + }, + + /* Best standard a client has shown: capability flags from hostapd + (ht/vht/he/eht) and the rate flags of the frames seen by iwinfo. + The last frame is often a low-rate management frame, so the + maximum is what the client can do. */ + clientStd: function(rx, tx, caps) { + var order = [ 'legacy', 'HT', 'VHT', 'HE', 'EHT' ], best = -1; + [ this.rateStd(rx), this.rateStd(tx) ].forEach(function(s) { best = Math.max(best, order.indexOf(s)); }); + if (caps && typeof caps === 'object') { + if (caps.eht) best = Math.max(best, 4); + else if (caps.he) best = Math.max(best, 3); + else if (caps.vht) best = Math.max(best, 2); + else if (caps.ht) best = Math.max(best, 1); + } + return best < 0 ? null : order[best]; + }, + + /* iwinfo rate block -> { rate (kbit/s), mhz, std, mcs, nss, label } */ + phy: function(r) { + if (!r || typeof r !== 'object') return null; + var std = this.rateStd(r), mhz = num(r.mhz) || null, rate = num(r.rate) || null; + var parts = [ fmt.phyRate(rate) ]; + if (mhz) parts.push(_('%s MHz').format(mhz)); + parts.push(std === 'legacy' ? _('legacy') : std); + var detail = []; + if (r.mcs != null && isFinite(+r.mcs)) detail.push(_('MCS %d').format(+r.mcs)); + if (r.nss != null && +r.nss > 0) detail.push((+r.nss) + '×' + (+r.nss)); + if (r.short_gi || +r.he_gi > 0 || +r.eht_gi > 0) detail.push(_('short GI')); + return { rate: rate, mhz: mhz, std: std, mcs: r.mcs != null ? +r.mcs : null, nss: r.nss != null ? +r.nss : null, + label: parts.join(' · '), detail: detail.join(' · ') }; + }, + + /* signal (dBm) -> { bars 0..4, level ok|warn|crit|none, word } */ + signal: function(dbm) { + dbm = num(dbm); + if (!dbm || dbm >= 0) return { bars: 0, level: 'none', word: _('No signal') }; + if (dbm >= -55) return { bars: 4, level: 'ok', word: _('Excellent') }; + if (dbm >= -67) return { bars: 3, level: 'ok', word: _('Good') }; + if (dbm >= -75) return { bars: 2, level: 'warn', word: _('Fair') }; + return { bars: 1, level: 'crit', word: _('Weak') }; + }, + + /* ath12k (and some others) report a fixed placeholder (-107..-121 dBm) + instead of a measured noise floor. Anything below -105 dBm is not + physical for a 20 MHz+ channel: unknown, not a fake SNR. */ + noise: function(dbm) { + dbm = num(dbm); + return (dbm < 0 && dbm > -105) ? dbm : null; + }, + + /* UCI encryption string -> { short, long, tone: ok|weak|open|owe } */ + security: function(enc) { + var e = String(enc == null ? 'none' : enc).trim().toLowerCase().replace(/\+.*$/, '') || 'none'; + for (var i = 0; i < SECURITY.length; i++) + if (SECURITY[i][0].test(e)) return { short: SECURITY[i][1], long: SECURITY[i][2], tone: SECURITY[i][3] }; + return { short: e.toUpperCase(), long: e, tone: 'ok' }; + }, + + /* channel -> centre frequency (MHz) for the band */ + chanFreq: function(band, chan) { + chan = num(chan); + if (!own(BANDS, band) || !chan) return null; + if (band === '2g') return chan === 14 ? 2484 : 2407 + 5 * chan; + if (band === '60g') return 56160 + 2160 * chan; + return BANDS[band].base + 5 * chan; + }, + + /* Where the occupied channel sits inside its band, as fractions 0..1 + of the band's span: { from, to, lo, hi, centre } or null. Uses the + centre channel of wide channels (iwinfo center_chan1). */ + spectrum: function(band, chan, centerChan, width) { + if (!own(BANDS, band)) return null; + var b = BANDS[band]; + var c = this.chanFreq(band, centerChan) || this.chanFreq(band, chan); + width = num(width) || 20; + if (!c) return null; + var lo = c - width / 2, hi = c + width / 2, span = b.hi - b.lo; + var clamp = function(v) { return Math.max(0, Math.min(1, v)); }; + return { from: clamp((lo - b.lo) / span), to: clamp((hi - b.lo) / span), lo: lo, hi: hi, centre: c, bandLo: b.lo, bandHi: b.hi }; + } +}); diff --git a/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js b/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js new file mode 100644 index 000000000..cfb825db3 --- /dev/null +++ b/luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js @@ -0,0 +1,1968 @@ +'use strict'; +'require view'; +'require rpc'; +'require poll'; +'require ui'; +'require dom'; +'require vantage.fmt as fmt'; +'require vantage.wifi as wifi'; +'require vantage.names as names'; +'require vantage.model as model'; +'require vantage.insight as insight'; +'require vantage.series as series'; + +/* Vantage dashboard (luci-app-vantage): a live, read-only overview of the + device, its uplink, radios, SSIDs and stations. The only write is the + station alias (uci vantage, own ACL). All DOM is built with E() and + text nodes; no string is ever parsed as markup. */ + +var POLL_S = 5; +var WINDOW_MS = 5 * 60 * 1000; /* throughput history shown */ +var HIST_MAX = 90; +var GONE_KEEP_MS = 5 * 60 * 1000; /* disconnected stations stay listed */ +var STORE_KEY = 'vantage.hist.v1'; +var SVGNS = 'http://www.w3.org/2000/svg'; +var HOSTAPD_OBJ = /^[A-Za-z0-9_.-]{1,64}$/; + +var callBoard = rpc.declare({ object: 'system', method: 'board' }); +var callInfo = rpc.declare({ object: 'system', method: 'info' }); +var callRead = rpc.declare({ object: 'file', method: 'read', params: [ 'path' ], expect: { data: '' } }); +var callIfDump = rpc.declare({ object: 'network.interface', method: 'dump', expect: { 'interface': [] } }); +var callDevs = rpc.declare({ object: 'network.device', method: 'status', expect: { '': {} } }); +/* luci.vantage is the app's own rpcd ucode plugin: network.wireless status + without keys or RADIUS secrets, and the validated alias write */ +var callWifi = rpc.declare({ object: 'luci.vantage', method: 'wireless', expect: { '': {} } }); +var callHints = rpc.declare({ object: 'luci-rpc', method: 'getHostHints', expect: { '': {} } }); +var callIwInfo = rpc.declare({ object: 'iwinfo', method: 'info', params: [ 'device' ], expect: { '': {} } }); +var callAssoc = rpc.declare({ object: 'iwinfo', method: 'assoclist', params: [ 'device' ], expect: { results: [] } }); +var callRrdns = rpc.declare({ object: 'network.rrdns', method: 'lookup', params: [ 'addrs', 'timeout', 'limit' ], expect: { '': {} } }); +var callMdns = rpc.declare({ object: 'umdns', method: 'hosts' }); +var callUciGet = rpc.declare({ object: 'uci', method: 'get', params: [ 'config' ], expect: { values: {} } }); +var callSetAlias = rpc.declare({ object: 'luci.vantage', method: 'set_alias', params: [ 'mac', 'name', 'icon' ] }); +var callAccess = rpc.declare({ object: 'session', method: 'access', params: [ 'scope', 'object', 'function' ], expect: { access: false } }); + +var hostapdCalls = {}; +function hostapd(ifname, method) { + var obj = 'hostapd.' + ifname, key = obj + ' ' + method; + if (!HOSTAPD_OBJ.test(obj)) return Promise.resolve(null); + if (!hostapdCalls[key]) hostapdCalls[key] = rpc.declare({ object: obj, method: method, expect: { '': {} } }); + return L.resolveDefault(hostapdCalls[key](), null); +} + +var cssPromise = null; +function loadCss() { + if (cssPromise) return cssPromise; + cssPromise = fetch(L.resource('vantage/app.css')).then(function(r) { return r.ok ? r.text() : ''; }).then(function(text) { + if (!text || !('adoptedStyleSheets' in document) || typeof CSSStyleSheet !== 'function') return; + var sheet = new CSSStyleSheet(); + sheet.replaceSync(text); + document.adoptedStyleSheets = document.adoptedStyleSheets.concat([ sheet ]); + }).catch(function() {}); + return cssPromise; +} + +function reducedMotion() { + return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); +} + +/* ------------------------------------------------------------- SVG kit */ + +var ICONS = { + globe: [ 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18Z', 'M3.4 9.2h17.2M3.4 14.8h17.2', 'M12 3c2.4 2.5 3.6 5.5 3.6 9s-1.2 6.5-3.6 9c-2.4-2.5-3.6-5.5-3.6-9s1.2-6.5 3.6-9Z' ], + ap: [ 'M4.5 15h15a1.5 1.5 0 0 1 1.5 1.5v1.5a1.5 1.5 0 0 1-1.5 1.5h-15A1.5 1.5 0 0 1 3 18v-1.5A1.5 1.5 0 0 1 4.5 15Z', 'M7 17.25h.01', 'M8.6 10.6a4.8 4.8 0 0 1 6.8 0', 'M6.1 8.1a8.4 8.4 0 0 1 11.8 0', 'M11.3 12.9a1 1 0 0 1 1.4 0' ], + radio: [ 'M12 12.5v8', 'M8.5 20.5h7', 'M9.2 8.7a4 4 0 0 0 0 5.6', 'M14.8 8.7a4 4 0 0 1 0 5.6', 'M6.4 5.9a8 8 0 0 0 0 11.2', 'M17.6 5.9a8 8 0 0 1 0 11.2', 'M12 10.3a1.2 1.2 0 1 1 0 2.4a1.2 1.2 0 1 1 0-2.4Z' ], + wifi: [ 'M3.3 9.4a12.3 12.3 0 0 1 17.4 0', 'M6.3 12.4a8 8 0 0 1 11.4 0', 'M9.3 15.4a3.8 3.8 0 0 1 5.4 0', 'M12 18.6h.01' ], + cpu: [ 'M7.5 6h9A1.5 1.5 0 0 1 18 7.5v9a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 16.5v-9A1.5 1.5 0 0 1 7.5 6Z', 'M10 10h4v4h-4z', 'M9.5 3v3M14.5 3v3M9.5 18v3M14.5 18v3M3 9.5h3M3 14.5h3M18 9.5h3M18 14.5h3' ], + memory: [ 'M4 7.5h16a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z', 'M7 16.5v2.5M11 16.5v2.5M15 16.5v2.5M19 16.5v2.5', 'M6.5 10.5h3v3h-3zM10.5 10.5h3v3h-3zM14.5 10.5h3v3h-3z' ], + disk: [ 'M4.5 6.5c0-1.4 3.4-2.5 7.5-2.5s7.5 1.1 7.5 2.5v11c0 1.4-3.4 2.5-7.5 2.5s-7.5-1.1-7.5-2.5z', 'M4.5 6.5c0 1.4 3.4 2.5 7.5 2.5s7.5-1.1 7.5-2.5', 'M4.5 12c0 1.4 3.4 2.5 7.5 2.5s7.5-1.1 7.5-2.5' ], + clock: [ 'M12 3.5a8.5 8.5 0 1 0 0 17a8.5 8.5 0 1 0 0-17Z', 'M12 7.5V12l3 2' ], + down: [ 'M12 5v14', 'M6.5 13.5 12 19l5.5-5.5' ], + up: [ 'M12 19V5', 'M6.5 10.5 12 5l5.5 5.5' ], + close: [ 'M6.5 6.5l11 11M17.5 6.5l-11 11' ], + search: [ 'M10.5 4.5a6 6 0 1 0 0 12a6 6 0 1 0 0-12Z', 'M15 15l4.5 4.5' ], + chevron: [ 'M9.5 6l6 6-6 6' ], + out: [ 'M9 15 16.5 7.5', 'M10 7.5h6.5V14', 'M13 4.5H6A1.5 1.5 0 0 0 4.5 6v12A1.5 1.5 0 0 0 6 19.5h12a1.5 1.5 0 0 0 1.5-1.5v-7' ], + check: [ 'M5 12.5l4.5 4.5L19 7.5' ], + warn: [ 'M10.3 4.6 3 17.5A2 2 0 0 0 4.7 20.5h14.6A2 2 0 0 0 21 17.5L13.7 4.6a2 2 0 0 0-3.4 0Z', 'M12 9.5v4.5', 'M12 17h.01' ], + err: [ 'M12 3.5a8.5 8.5 0 1 0 0 17a8.5 8.5 0 1 0 0-17Z', 'M9 9l6 6M15 9l-6 6' ], + info: [ 'M12 3.5a8.5 8.5 0 1 0 0 17a8.5 8.5 0 1 0 0-17Z', 'M12 11v5.5', 'M12 7.8h.01' ], + pencil: [ 'M4.5 19.5h4l10-10-4-4-10 10z', 'M13 7l4 4' ], + lock: [ 'M6.5 11h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z', 'M8.5 11V8a3.5 3.5 0 0 1 7 0v3' ], + unlock: [ 'M6.5 11h11a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1v-7a1 1 0 0 1 1-1Z', 'M8.5 11V8a3.5 3.5 0 0 1 6.8-1.2' ], + users: [ 'M9 11a3.5 3.5 0 1 0 0-7a3.5 3.5 0 1 0 0 7Z', 'M2.5 20c.8-3.4 3.3-5.5 6.5-5.5s5.7 2.1 6.5 5.5', 'M16 4.3a3.5 3.5 0 0 1 0 6.4', 'M18 14.8c1.8.8 3 2.6 3.5 5.2' ], + spark: [ 'M12 3.5v4M12 16.5v4M3.5 12h4M16.5 12h4M6 6l2.6 2.6M15.4 15.4 18 18M6 18l2.6-2.6M15.4 8.6 18 6' ], + pulse: [ 'M3 12h4l2.5-6 5 12 2.5-6H21' ], + phone: [ 'M8 3h8a1.5 1.5 0 0 1 1.5 1.5v15A1.5 1.5 0 0 1 16 21H8a1.5 1.5 0 0 1-1.5-1.5v-15A1.5 1.5 0 0 1 8 3Z', 'M11 18h2' ], + laptop: [ 'M5.5 5.5h13a1 1 0 0 1 1 1v8.5h-15V6.5a1 1 0 0 1 1-1Z', 'M2.5 18.5h19l-1.5-3.5h-16z' ], + tablet: [ 'M6.5 3h11A1.5 1.5 0 0 1 19 4.5v15a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 5 19.5v-15A1.5 1.5 0 0 1 6.5 3Z', 'M11 18h2' ], + desktop: [ 'M4.5 4.5h15a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-9a1 1 0 0 1 1-1Z', 'M9 20h6', 'M12 15.5V20' ], + tv: [ 'M4 6h16a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1Z', 'M8 20.5h8' ], + speaker: [ 'M7.5 3h9A1.5 1.5 0 0 1 18 4.5v15a1.5 1.5 0 0 1-1.5 1.5h-9A1.5 1.5 0 0 1 6 19.5v-15A1.5 1.5 0 0 1 7.5 3Z', 'M12 7h.01', 'M12 11a3 3 0 1 0 0 6a3 3 0 1 0 0-6Z' ], + printer: [ 'M7 8V3.5h10V8', 'M7 16.5H4.5A1.5 1.5 0 0 1 3 15V9.5A1.5 1.5 0 0 1 4.5 8h15A1.5 1.5 0 0 1 21 9.5V15a1.5 1.5 0 0 1-1.5 1.5H17', 'M7 13h10v7.5H7z' ], + camera: [ 'M4.5 7.5h3l1.5-2h6l1.5 2h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1Z', 'M12 10a3.2 3.2 0 1 0 0 6.4a3.2 3.2 0 1 0 0-6.4Z' ], + console: [ 'M7.5 8h9a4.5 4.5 0 0 1 4.5 4.5v.5a3.5 3.5 0 0 1-6.3 2.1L14 14h-4l-.7 1.1A3.5 3.5 0 0 1 3 13v-.5A4.5 4.5 0 0 1 7.5 8Z', 'M7.5 10.5v3M6 12h3', 'M15.5 11.5h.01M17.5 13h.01' ], + watch: [ 'M8.5 6h7A1.5 1.5 0 0 1 17 7.5v9a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 7 16.5v-9A1.5 1.5 0 0 1 8.5 6Z', 'M9 6l.5-3h5l.5 3M9 18l.5 3h5l.5-3' ], + iot: [ 'M9 18h6', 'M10 21h4', 'M12 3a6 6 0 0 0-3.5 10.9V16h7v-2.1A6 6 0 0 0 12 3Z' ], + router: [ 'M4.5 13h15a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1Z', 'M7 16h.01M10 16h.01', 'M8 13 6 6.5M16 13l2-6.5' ], + device: [ 'M7 3.5h10A1.5 1.5 0 0 1 18.5 5v14a1.5 1.5 0 0 1-1.5 1.5H7A1.5 1.5 0 0 1 5.5 19V5A1.5 1.5 0 0 1 7 3.5Z', 'M10 17.5h4', 'M9 7.5h6' ] +}; + +function svgRoot(cls, viewBox) { + var s = document.createElementNS(SVGNS, 'svg'); + s.setAttribute('class', cls); + s.setAttribute('viewBox', viewBox); + s.setAttribute('aria-hidden', 'true'); + s.setAttribute('focusable', 'false'); + return s; +} + +function svgPath(d, cls) { + var p = document.createElementNS(SVGNS, 'path'); + p.setAttribute('d', d); + if (cls) p.setAttribute('class', cls); + return p; +} + +function svgRect(x, y, w, h, rx, cls) { + var r = document.createElementNS(SVGNS, 'rect'); + r.setAttribute('x', String(x)); + r.setAttribute('y', String(y)); + r.setAttribute('width', String(w)); + r.setAttribute('height', String(h)); + r.setAttribute('rx', String(rx)); + if (cls) r.setAttribute('class', cls); + return r; +} + +function icon(name, cls) { + var s = svgRoot('vt-ico' + (cls ? ' ' + cls : ''), '0 0 24 24'); + var list = Object.prototype.hasOwnProperty.call(ICONS, name) ? ICONS[name] : ICONS.device; + list.forEach(function(d) { s.appendChild(svgPath(d)); }); + return s; +} + +/* four rising bars, `bars` of them lit, coloured by level */ +function signalGlyph(sig) { + var s = svgRoot('vt-sig vt-lvl-' + sig.level, '0 0 20 16'); + for (var i = 0; i < 4; i++) + s.appendChild(svgRect(1 + i * 5, 12 - i * 3.5, 3.4, 3 + i * 3.5, 1, i < sig.bars ? 'vt-sig-on' : 'vt-sig-off')); + return s; +} + +/* vertical fade for area fills: the series colour (--vt-sc) at the + chart fill opacity at the top, transparent at the baseline. Returns the + paint reference for the fill attribute. */ +var gradSeq = 0; +function fadeFill(svg) { + var id = 'vt-fade-' + (++gradSeq); + var defs = document.createElementNS(SVGNS, 'defs'); + var lg = document.createElementNS(SVGNS, 'linearGradient'); + lg.setAttribute('id', id); + lg.setAttribute('x1', '0'); lg.setAttribute('y1', '0'); + lg.setAttribute('x2', '0'); lg.setAttribute('y2', '1'); + [ [ '0', 'vt-fade-top' ], [ '1', 'vt-fade-end' ] ].forEach(function(st) { + var stop = document.createElementNS(SVGNS, 'stop'); + stop.setAttribute('offset', st[0]); + stop.setAttribute('class', st[1]); + lg.appendChild(stop); + }); + defs.appendChild(lg); + svg.appendChild(defs); + return 'url(#' + id + ')'; +} + +function pos(el, xPct, yPct) { + el.style.left = Math.max(0, Math.min(100, xPct)).toFixed(2) + '%'; + el.style.top = Math.max(0, Math.min(100, yPct)).toFixed(2) + '%'; + return el; +} + +/* Compact chart in the dashboard's chart language: the first column as a + line over a fading area, a second column as a faint dashed line, a + marker on the newest value, a faint top grid line and (opts.scale) the + axis maximum with its unit. Before a minute of history the data is + drawn from the right edge. opts: { min: axis floor, scale: fn(max), + thresh: value for a dashed reference line, threshLabel } */ +function sparkline(data, cols, vmax, cls, opts) { + opts = opts || {}; + var W = 100, H = 26; + var wrap = E('div', { 'class': 'vt-sk' + (cls ? ' ' + cls : '') }); + var s = svgRoot('vt-spark', '0 0 ' + W + ' ' + H); + s.setAttribute('preserveAspectRatio', 'none'); + wrap.appendChild(s); + s.appendChild(svgPath('M0 0H' + W, 'vt-spark-grid')); + s.appendChild(svgPath('M0 ' + H + 'H' + W, 'vt-spark-base')); + if (!data || data.length < 2) { + wrap.classList.add('vt-sk-empty'); + wrap.appendChild(E('span', { 'class': 'vt-sk-note' }, [ _('collecting…') ])); + return wrap; + } + var t1 = data[data.length - 1][0], t0 = Math.min(data[0][0], t1 - 60000); + var max = vmax > 0 ? vmax : series.niceCeil(Math.max(series.max(data, cols) * 1.05, opts.min || 0)); + if (opts.thresh != null) { + var ty = H - Math.max(0, Math.min(1, opts.thresh / max)) * H; + s.appendChild(svgPath('M0 ' + ty.toFixed(1) + 'H' + W, 'vt-spark-thresh')); + if (opts.threshLabel) wrap.appendChild(pos(E('span', { 'class': 'vt-sk-thresh' }, [ opts.threshLabel ]), 100, ty / H * 100)); + } + cols.slice().reverse().forEach(function(ci) { + var k = cols.indexOf(ci); + var segs = series.segments(data, ci, t0, t1, max, W, H, POLL_S * 3500); + var g = document.createElementNS(SVGNS, 'g'); + g.setAttribute('class', 'vt-spark-s' + k); + if (k === 0) { + var area = svgPath(series.areaPath(segs, H), 'vt-spark-area'); + area.setAttribute('fill', fadeFill(s)); + g.appendChild(area); + } + g.appendChild(svgPath(series.linePath(segs), 'vt-spark-line')); + s.appendChild(g); + if (k === 0) { + var last = segs.length ? segs[segs.length - 1] : null, pt = last && last[last.length - 1]; + if (pt) wrap.appendChild(pos(E('span', { 'class': 'vt-sk-dot' }), pt[0] / W * 100, pt[1] / H * 100)); + } + }); + if (opts.scale) wrap.appendChild(E('span', { 'class': 'vt-sk-max vt-tn' }, [ opts.scale(max) ])); + return wrap; +} + +/* -------------------------------------------------------------- helpers */ + +function txt(el, s) { el.textContent = (s == null ? '' : String(s)); return el; } +function setW(el, pct) { el.style.width = Math.max(0, Math.min(100, +pct || 0)).toFixed(2) + '%'; } +function hueOf(mac) { + var h = 0, s = String(mac || ''); + for (var i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) % 360; + return h; +} + +function avatar(c, small) { + var el = E('span', { 'class': 'vt-av' + (small ? ' vt-av-sm' : '') + (c.gone ? ' vt-av-gone' : '') }, [ icon(c.icon || 'device') ]); + el.style.setProperty('--vt-h', hueOf(c.mac).toFixed(0)); + return el; +} + +function bandBadge(band, label) { + return E('span', { 'class': 'vt-band vt-band-' + (band || 'x') }, [ label || wifi.bandLabel(band) ]); +} + +function pill(text, tone, title) { + var el = E('span', { 'class': 'vt-pill vt-pill-' + (tone || 'muted') }, [ text ]); + if (title) el.setAttribute('title', title); + return el; +} + +function rateLine(down, up, cls) { + return E('span', { 'class': 'vt-rates' + (cls ? ' ' + cls : '') }, [ + E('span', { 'class': 'vt-rate vt-rate-down', 'title': _('Download (to clients)') }, [ icon('down'), fmt.bits(down) ]), + E('span', { 'class': 'vt-rate vt-rate-up', 'title': _('Upload (from clients)') }, [ icon('up'), fmt.bits(up) ]) + ]); +} + +function kv(label, value, mono) { + return E('div', { 'class': 'vt-kv' }, [ + E('dt', {}, [ label ]), + E('dd', { 'class': mono ? 'vt-mono' : '' }, [].concat(value == null || value === '' ? fmt.DASH : value)) + ]); +} + +function tileHead(ic, title, extra) { + return E('header', { 'class': 'vt-tile-head' }, [ + E('h3', { 'class': 'vt-tile-title' }, [ icon(ic), E('span', {}, [ title ]) ]), + E('div', { 'class': 'vt-tile-extra' }, extra || []) + ]); +} + +function pageLink(page, label) { + return E('a', { 'class': 'vt-link', 'href': L.url.apply(L, page) }, [ E('span', {}, [ label ]), icon('chevron') ]); +} + +/* axis labels: round numbers without a trailing '.0' ('15 Mbit/s') */ +function bitsLabel(v) { + var p = fmt.bitsParts(v); + return p.u ? String(+p.v || p.v) + ' ' + p.u : p.v; +} + +/* '5 min ago' / '45 s ago' for the left end of a time axis */ +function spanAgo(ms) { + var s = Math.round(ms / 1000); + return s >= 90 ? _('%d min ago').format(Math.round(s / 60)) : _('%d s ago').format(s); +} + +/* '1 min' / '40 s' of collected history */ +function spanShort(ms) { + var s = Math.max(0, Math.round(ms / 1000)); + return s >= 60 ? _('%d min').format(Math.floor(s / 60)) : _('%d s').format(s); +} + +function readStore() { + try { return JSON.parse(window.sessionStorage.getItem(STORE_KEY) || 'null'); } catch (e) { return null; } +} + +function writeStore(v) { + try { window.sessionStorage.setItem(STORE_KEY, JSON.stringify(v)); } catch (e) {} +} + +/* Sections that fold on narrow screens; the choice is remembered per + browser. Defaults keep the phone page short: radios and the network + path start condensed. */ +var FOLD_KEY = 'vantage.fold.v1'; +var FOLD_DEFAULT = { radios: true, hero: true, ssids: false, chart: false, sys: false }; + +function readFolds() { + var out = Object.assign({}, FOLD_DEFAULT), v = null; + try { v = JSON.parse(window.localStorage.getItem(FOLD_KEY) || 'null'); } catch (e) { v = null; } + if (v && typeof v === 'object') + Object.keys(FOLD_DEFAULT).forEach(function(k) { if (typeof v[k] === 'boolean') out[k] = v[k]; }); + return out; +} + +function writeFolds(v) { + try { window.localStorage.setItem(FOLD_KEY, JSON.stringify(v)); } catch (e) {} +} + +/* ================================================================= view */ + +return view.extend({ + handleSave: null, + handleSaveApply: null, + handleReset: null, + + /* ---------------------------------------------------------- loading */ + + load: function() { + return Promise.all([ + loadCss(), + L.resolveDefault(callBoard(), {}), + L.resolveDefault(callAccess('access-group', 'luci-app-vantage-names', 'write'), false), + this.loadAliases(), + L.resolveDefault(callMdns(), null), + this.fetch(), + /* reverse DNS is an optional grant: without it, skip it quietly */ + L.resolveDefault(callAccess('access-group', 'luci-app-vantage-rdns', 'read'), false) + ]).then(function(r) { + return { board: r[1] || {}, canWrite: r[2] === true, mdns: r[4], raw: r[5], canRdns: r[6] === true }; + }); + }, + + loadAliases: function() { + var self = this; + return L.resolveDefault(callUciGet('vantage'), {}).then(function(values) { + var list = (values && typeof values === 'object') ? Object.keys(values).map(function(k) { return values[k]; }) : []; + self.aliases = names.aliasMap(list.slice(0, names.ALIAS_MAX + 1)); + return self.aliases; + }); + }, + + /* one poll's worth of raw replies; missing pieces degrade to {} */ + fetch: function() { + var started = Date.now(); + return Promise.all([ + L.resolveDefault(callInfo(), {}), + L.resolveDefault(callRead('/proc/stat'), ''), + L.resolveDefault(callIfDump(), []), + L.resolveDefault(callDevs(), {}), + L.resolveDefault(callWifi(), {}), + L.resolveDefault(callHints(), {}) + ]).then(function(r) { + var wifiDevs = (r[4] && typeof r[4] === 'object') ? r[4] : {}; + var ifnames = []; + Object.keys(wifiDevs).forEach(function(rn) { + var ifs = (wifiDevs[rn] && Array.isArray(wifiDevs[rn].interfaces)) ? wifiDevs[rn].interfaces : []; + ifs.forEach(function(i) { + if (i && typeof i.ifname === 'string' && HOSTAPD_OBJ.test(i.ifname) && ifnames.indexOf(i.ifname) < 0) ifnames.push(i.ifname); + }); + }); + /* iwinfo per interface: channel and power for the radio, BSSID + and SSID for each network (luci.vantage wireless carries + configuration only) */ + var per = []; + ifnames.forEach(function(ifn) { + per.push(L.resolveDefault(callAssoc(ifn), []).then(function(v) { return [ 'assoc', ifn, v ]; })); + per.push(hostapd(ifn, 'get_clients').then(function(v) { return [ 'hapd', ifn, v ]; })); + per.push(hostapd(ifn, 'get_status').then(function(v) { return [ 'hapdStatus', ifn, v ]; })); + per.push(L.resolveDefault(callIwInfo(ifn), {}).then(function(v) { return [ 'iwinfo', ifn, v ]; })); + }); + return Promise.all(per).then(function(parts) { + var assoc = Object.create(null), hapd = Object.create(null), hapdStatus = Object.create(null), iwinfo = Object.create(null); + parts.forEach(function(p) { + if (p[0] === 'assoc') assoc[p[1]] = p[2]; + else if (p[0] === 'hapd') hapd[p[1]] = p[2]; + else if (p[0] === 'hapdStatus') hapdStatus[p[1]] = p[2]; + else iwinfo[p[1]] = p[2]; + }); + return { info: r[0] || {}, stat: r[1] || '', ifaces: r[2] || [], devs: r[3] || {}, wifi: wifiDevs, hints: r[5] || {}, + assoc: assoc, hapd: hapd, hapdStatus: hapdStatus, iwinfo: iwinfo, at: Date.now(), started: started }; + }); + }); + }, + + /* ------------------------------------------------------------ state */ + + initState: function(data) { + var stored = readStore() || {}, now = Date.now(); + /* keys come from sessionStorage: no prototype to overwrite */ + var net = Object.create(null); + if (stored.net && typeof stored.net === 'object') + Object.keys(stored.net).forEach(function(k) { + if (/^[A-Za-z0-9_.:-]{1,40}$/.test(k)) net[k] = series.valid(stored.net[k], 3, HIST_MAX, now); + }); + var clientHist = Object.create(null); + if (stored.clients && typeof stored.clients === 'object') + Object.keys(stored.clients).slice(0, 64).forEach(function(k) { + var mac = names.normMac(k); + if (mac) clientHist[mac] = series.valid(stored.clients[k], 4, 150, now); + }); + this.board = data.board || {}; + this.canWrite = data.canWrite; + this.st = { + clock: null, stat: null, cpu: null, + devPrev: null, staPrev: null, devRates: Object.create(null), staRates: Object.create(null), radioRates: Object.create(null), ssidRates: Object.create(null), + hist: { net: net, cpu: series.valid(stored.cpu, 2, HIST_MAX, now), mem: series.valid(stored.mem, 2, HIST_MAX, now) }, + clientHist: clientHist, bestStd: Object.create(null), + presence: null, isNew: Object.create(null), gone: [], + rdns: Object.create(null), rdnsBusy: false, rdnsOff: !data.canRdns, + mdns: Object.create(null), mdnsOff: true, mdnsAt: 0, + oui: null, ouiLoading: false, + lastOk: 0, lastErr: null, paused: false + }; + this.ui = { sort: 'live', dir: -1, filter: '', chip: 'all', chart: 'down', drawer: null, editing: null, hover: null }; + this.folds = readFolds(); + this.foldEls = {}; + this.foldBtns = {}; + this.applyMdns(data.mdns); + this.aliases = this.aliases || {}; + }, + + applyMdns: function(reply) { + /* umdns missing (NOT_FOUND / no permission) answers a number */ + if (reply && typeof reply === 'object') { this.st.mdns = names.mdnsMap(reply); this.st.mdnsOff = false; } + else this.st.mdnsOff = true; + this.st.mdnsAt = Date.now(); + }, + + /* raw replies -> model + rates + histories */ + ingest: function(raw) { + var st = this.st, self = this; + var now = raw.at || Date.now(); + st.clock = model.sampleClock(st.clock, now, raw.info && raw.info.localtime); + var at = st.clock.at; + + /* CPU */ + var stat = model.parseStat(raw.stat); + if (stat) { + var cpu = model.cpuUsage(st.stat, stat); + if (cpu) { st.cpu = cpu; series.push(st.hist.cpu, [ at, cpu.pct ], WINDOW_MS, HIST_MAX); } + if (!st.stat || stat.all.total !== st.stat.all.total) st.stat = stat; + } + + var m = model.build({ + board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo, + assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, + aliases: this.aliases, rdns: st.rdns, mdns: st.mdns, + vendor: st.oui ? function(k) { return k ? st.oui.lookup(k) : null; } : null + }); + if (m.device.memory) series.push(st.hist.mem, [ at, m.device.memory.usedPct ], WINDOW_MS, HIST_MAX); + + /* interface counters */ + var devCounters = Object.create(null); + Object.keys(raw.devs || {}).forEach(function(n) { + var s = raw.devs[n] && raw.devs[n].statistics; + if (s) devCounters[n] = { rx: s.rx_bytes, tx: s.tx_bytes }; + }); + var dr = model.rates(st.devPrev, devCounters, at); + st.devPrev = dr.next; + if (Object.keys(dr.rates).length || !Object.keys(devCounters).length) st.devRates = dr.rates; + + function sum(list) { + var rx = 0, tx = 0, ok = false; + list.forEach(function(n) { var r = st.devRates[n]; if (r) { rx += r.rx; tx += r.tx; ok = true; } }); + return ok ? { rx: rx, tx: tx } : null; + } + st.radioRates = Object.create(null); + m.radios.forEach(function(r) { var v = sum(r.ifnames); if (v) st.radioRates[r.id] = v; }); + st.ssidRates = Object.create(null); + m.ssids.forEach(function(s) { var v = s.ifname ? sum([ s.ifname ]) : null; if (v) st.ssidRates[s.id] = v; }); + var upRate = m.uplink ? st.devRates[m.uplink.statsDev] : null; + st.uplinkRate = upRate || null; + + /* throughput history (only for ticks that produced a rate) */ + if (upRate) this.pushNet('uplink', at, upRate); + m.radios.forEach(function(r) { if (st.radioRates[r.id]) self.pushNet(r.id, at, st.radioRates[r.id]); }); + + /* station counters, per-station history */ + var staCounters = Object.create(null); + m.clients.forEach(function(c) { staCounters[c.mac] = { rx: c.rxBytes, tx: c.txBytes }; }); + var sr = model.rates(st.staPrev, staCounters, at); + st.staPrev = sr.next; + if (Object.keys(sr.rates).length || !m.clients.length) st.staRates = sr.rates; + m.clients.forEach(function(c) { + var h = st.clientHist[c.mac] || (st.clientHist[c.mac] = []); + var r = st.staRates[c.mac]; + series.push(h, [ at, c.signal != null ? -c.signal : NaN, r ? r.tx : NaN, r ? r.rx : NaN ], 10 * 60 * 1000, 150); + }); + + /* presence: new since the page opened, recently gone */ + var p = insight.presence(st.presence, m.clients, now, GONE_KEEP_MS); + st.presence = p.state; + st.isNew = p.isNew; + st.gone = p.gone; + Object.keys(st.clientHist).forEach(function(mac) { + if (!p.state.firstSeen[mac]) delete st.clientHist[mac]; + }); + + /* per-station history is kept for the session (60 points each, + stations seen in the last minutes only) */ + var keepClients = {}; + Object.keys(st.clientHist).slice(0, 32).forEach(function(mac) { + keepClients[mac] = st.clientHist[mac].slice(-60).filter(function(p) { return p.every(function(v) { return isFinite(v); }); }); + }); + writeStore({ net: st.hist.net, cpu: st.hist.cpu, mem: st.hist.mem, clients: keepClients }); + + this.stickyGen(m); + m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = p.state.firstSeen[c.mac]; c.isNew = !!p.isNew[c.mac]; }); + st.health = insight.health(m, { cpu: st.cpu, uplinkRate: st.uplinkRate }); + st.summary = insight.summary(st.health); + st.radioInsights = insight.radioInsights(m.radios, st.radioRates); + st.talkers = insight.topTalkers(m.clients, st.staRates, 5); + this.m = m; + this.lastRaw = raw; + this.resolveMore(m); + return m; + }, + + /* The generation shown is the best standard a station used this + session: single frames (management, retries) go out at legacy + rates and would make the badge flicker. */ + stickyGen: function(m) { + var best = this.st.bestStd, order = [ 'legacy', 'HT', 'VHT', 'HE', 'EHT' ]; + m.clients.forEach(function(c) { + var cur = order.indexOf(c.std), prev = order.indexOf(best[c.mac]); + if (prev > cur) { c.std = best[c.mac]; c.gen = wifi.genName(c.std, c.band); } + else if (cur >= 0) best[c.mac] = c.std; + }); + }, + + pushNet: function(key, at, r) { + var h = this.st.hist.net[key] || (this.st.hist.net[key] = []); + series.push(h, [ at, r.rx, r.tx ], WINDOW_MS, HIST_MAX); + }, + + /* names beyond what one poll carries: reverse DNS once per new + address, mDNS every minute when present, the vendor table once */ + resolveMore: function(m) { + var st = this.st, self = this; + var ips = []; + m.clients.forEach(function(c) { + c.ips.forEach(function(ip) { if (!(ip in st.rdns) && ips.indexOf(ip) < 0 && ips.length < 64) ips.push(ip); }); + }); + if (ips.length && !st.rdnsBusy && !st.rdnsOff) { + st.rdnsBusy = true; + ips.forEach(function(ip) { st.rdns[ip] = ''; }); + /* rpcd-mod-rrdns is optional: when the object or the grant is + missing the first call says so and reverse DNS stays off; + a timeout or a network error only skips this batch */ + callRrdns(ips, 1500, 1000).catch(function(e) { + if (/ubus code [2-6]\b|error -320\d\d\b/.test(String(e && e.message))) st.rdnsOff = true; + return {}; + }).then(function(res) { + var hit = false; + if (res && typeof res === 'object') + Object.keys(res).forEach(function(ip) { if (typeof res[ip] === 'string' && ips.indexOf(ip) >= 0) { st.rdns[ip] = res[ip]; hit = true; } }); + if (hit) self.rerender(); + }).finally(function() { st.rdnsBusy = false; }); + } + if (!st.mdnsOff && Date.now() - st.mdnsAt > 60000) { + st.mdnsAt = Date.now(); + L.resolveDefault(callMdns(), null).then(function(r) { self.applyMdns(r); }); + } + var wantVendor = m.clients.some(function(c) { return c.nameSource === 'mac'; }); + if (wantVendor && !st.oui && !st.ouiLoading) { + st.ouiLoading = true; + L.require('vantage.oui').then(function(mod) { st.oui = mod; self.rerender(); }).catch(function() {}); + } + }, + + /* rebuild the model from the last replies (names changed) */ + rerender: function() { + if (!this.lastRaw) return; + var st = this.st, raw = this.lastRaw; + var m = model.build({ + board: this.board, info: raw.info, ifaces: raw.ifaces, devs: raw.devs, wifi: raw.wifi, iwinfo: raw.iwinfo, + assoc: raw.assoc, hapd: raw.hapd, hapdStatus: raw.hapdStatus, hints: raw.hints, + aliases: this.aliases, rdns: st.rdns, mdns: st.mdns, + vendor: st.oui ? function(k) { return k ? st.oui.lookup(k) : null; } : null + }); + this.stickyGen(m); + m.clients.forEach(function(c) { c.exp = insight.experience(c); c.firstSeen = st.presence && st.presence.firstSeen[c.mac]; c.isNew = !!st.isNew[c.mac]; }); + st.talkers = insight.topTalkers(m.clients, st.staRates, 5); + st.health = insight.health(m, { cpu: st.cpu, uplinkRate: st.uplinkRate }); + st.summary = insight.summary(st.health); + this.m = m; + this.paint(); + }, + + refresh: function() { + var self = this; + if (document.hidden) { this.st.paused = true; this.paintLive(); return Promise.resolve(); } + this.st.paused = false; + return this.fetch().then(function(raw) { + self.ingest(raw); + self.st.lastOk = Date.now(); + self.st.lastErr = null; + self.paint(); + }).catch(function(err) { + self.st.lastErr = err; + self.paintLive(); + }); + }, + + /* ----------------------------------------------------------- render */ + + render: function(data) { + var self = this; + this.initState(data); + this.ingest(data.raw); + this.st.lastOk = Date.now(); + + this.el = {}; + /* .vt-body is the size container for the layout; the drawer stays + outside it (a size container would trap position: fixed) */ + var root = E('div', { 'class': 'vt-app', 'id': 'vt-app' }, [ + E('div', { 'class': 'vt-body' }, [ + this.buildHead(), + this.el.health = E('div', { 'class': 'vt-health', 'role': 'list', 'aria-label': _('Health checks') }), + E('div', { 'class': 'vt-grid' }, [ + this.buildHero(), + this.buildSystem(), + this.buildChart(), + this.buildRadios(), + this.buildClients(), + E('div', { 'class': 'vt-side' }, [ this.buildTalkers(), this.buildSsids() ]) + ]) + ]), + this.buildDrawer() + ]); + this.el.root = root; + + this.paint(); + + poll.add(L.bind(this.refresh, this), POLL_S); + document.addEventListener('visibilitychange', function() { + if (!document.hidden && self.st.paused) self.refresh(); + else self.paintLive(); + }); + document.addEventListener('keydown', L.bind(this.handleKey, this)); + window.setInterval(function() { self.paintLive(); }, 1000); + if (window.ResizeObserver) new ResizeObserver(function() { self.scheduleWires(); }).observe(this.el.path); + window.addEventListener('hashchange', function() { self.openFromHash(); }); + window.requestAnimationFrame(function() { self.drawWires(); self.openFromHash(); }); + return root; + }, + + paint: function() { + this.paintHead(); + this.paintHealth(); + this.paintHero(); + this.paintSystem(); + this.paintChart(); + this.paintRadios(); + this.paintClients(); + this.paintTalkers(); + this.paintSsids(); + this.paintDrawer(); + this.paintLive(); + this.scheduleWires(); + }, + + /* ------------------------------------------------------------ folds */ + + foldButton: function(key, label) { + var self = this; + var btn = E('button', { 'type': 'button', 'class': 'vt-fold-btn', 'aria-expanded': 'true', 'aria-label': label, 'title': label, + 'click': function() { self.toggleFold(key); } }, [ icon('chevron') ]); + this.foldBtns[key] = btn; + return btn; + }, + + foldable: function(key, el) { + this.foldEls[key] = el; + el.classList.add('vt-fold'); + this.applyFold(key); + return el; + }, + + applyFold: function(key) { + var folded = !!this.folds[key], el = this.foldEls[key], btn = this.foldBtns[key]; + if (el) el.classList.toggle('vt-folded', folded); + if (btn) btn.setAttribute('aria-expanded', String(!folded)); + }, + + toggleFold: function(key) { + this.folds[key] = !this.folds[key]; + writeFolds(this.folds); + this.applyFold(key); + if (key === 'hero') this.scheduleWires(); + if (key === 'chart') this.paintChart(); + }, + + /* ------------------------------------------------------------- head */ + + buildHead: function() { + var el = this.el; + return E('header', { 'class': 'vt-head' }, [ + el.orb = E('span', { 'class': 'vt-orb' }, [ E('span', { 'class': 'vt-orb-core' }) ]), + E('div', { 'class': 'vt-head-text' }, [ + el.headline = E('h2', { 'class': 'vt-headline' }), + el.subline = E('p', { 'class': 'vt-subline' }) + ]), + el.live = E('div', { 'class': 'vt-livepill', 'role': 'status', 'aria-live': 'off' }, [ + E('span', { 'class': 'vt-live-beat' }), + el.liveText = E('span', { 'class': 'vt-live-text' }) + ]) + ]); + }, + + paintHead: function() { + var s = this.st.summary, d = this.m.device; + this.el.orb.className = 'vt-orb vt-orb-' + s.level; + txt(this.el.headline, s.headline); + var clients = this.m.clients.length; + txt(this.el.subline, [ d.model, d.hostname, d.firmware ? d.firmware.replace(/\s+r\d+-[0-9a-f]+$/, '') : null, + clients === 1 ? _('1 client') : _('%d clients').format(clients) ].filter(Boolean).join(' · ')); + }, + + paintLive: function() { + if (!this.el || !this.el.live) return; + var st = this.st, state, label; + if (st.paused || document.hidden) { state = 'paused'; label = _('Paused while hidden'); } + else if (st.lastErr) { state = 'err'; label = _('Reconnecting…'); } + else { state = 'ok'; label = _('Live') + ' · ' + fmt.ago(Date.now() - st.lastOk); } + this.el.live.className = 'vt-livepill vt-live-' + state; + this.el.live.setAttribute('title', label); + txt(this.el.liveText, label); + }, + + /* ----------------------------------------------------------- health */ + + paintHealth: function() { + var self = this; + dom.content(this.el.health, this.st.health.map(function(c) { + var ic = c.level === 'ok' ? 'check' : c.level === 'err' ? 'err' : c.level === 'warn' ? 'warn' : 'info'; + var body = [ + E('span', { 'class': 'vt-chip-ico' }, [ icon(ic) ]), + E('span', { 'class': 'vt-chip-main' }, [ + E('span', { 'class': 'vt-chip-top' }, [ E('span', { 'class': 'vt-chip-title' }, [ c.title ]), E('span', { 'class': 'vt-chip-value' }, [ c.value ]) ]), + E('span', { 'class': 'vt-chip-reason' }, [ c.reason ]) + ]) + ]; + if (c.client) + return E('button', { 'type': 'button', 'class': 'vt-chip vt-chip-' + c.level, 'role': 'listitem', 'title': c.detail || c.reason, + 'click': function() { self.openDrawer('client', c.client); } }, body); + return E('a', { 'class': 'vt-chip vt-chip-' + c.level, 'role': 'listitem', 'title': c.detail || c.reason, 'href': L.url.apply(L, c.page) }, body); + })); + }, + + /* ------------------------------------------------------------- hero */ + + buildHero: function() { + var self = this, el = this.el; + el.path = E('div', { 'class': 'vt-path' }); + el.wires = svgRoot('vt-wires', '0 0 10 10'); + el.path.appendChild(el.wires); + this.wireMap = {}; + el.kpis = E('div', { 'class': 'vt-kpis vt-fold-keep' }); + return this.foldable('hero', E('section', { 'class': 'vt-tile vt-hero', 'aria-label': _('Network path') }, [ + tileHead('pulse', _('Network path'), [ E('span', { 'class': 'vt-hint vt-hint-wide' }, [ _('Click any node for details') ]), this.foldButton('hero', _('Show or hide the network path')) ]), + el.path, + el.kpis + ])); + }, + + paintHero: function() { + var self = this, m = this.m, st = this.st, up = m.uplink, path = this.el.path; + var up_r = st.uplinkRate; + + /* keep the wires layer, rebuild the nodes (cheap, a dozen elements) */ + while (path.lastChild && path.lastChild !== this.el.wires) path.removeChild(path.lastChild); + while (path.firstChild && path.firstChild !== this.el.wires) path.removeChild(path.firstChild); + + this.nodes = {}; + var gw = E('button', { 'type': 'button', 'class': 'vt-node vt-node-gw', 'click': function() { self.openDrawer('uplink', 'uplink'); } }, [ + E('span', { 'class': 'vt-node-ico' }, [ icon('globe') ]), + E('span', { 'class': 'vt-node-body' }, [ + E('span', { 'class': 'vt-node-kicker' }, [ up && up.isWan ? _('Internet') : _('Gateway') ]), + E('span', { 'class': 'vt-node-title' }, [ up && up.gateway ? up.gateway : _('No default route') ]), + E('span', { 'class': 'vt-node-sub' }, [ up && up.dns.length ? _('DNS %s').format(up.dns[0]) : _('Reachability not probed') ]) + ]) + ]); + this.nodes.gw = gw; + + var link = E('div', { 'class': 'vt-link-label' }, [ + E('span', { 'class': 'vt-link-rate vt-link-down' }, [ icon('down'), E('b', {}, [ up_r ? fmt.bitsParts(up_r.rx).v : fmt.DASH ]), E('small', {}, [ up_r ? fmt.bitsParts(up_r.rx).u : '' ]) ]), + E('span', { 'class': 'vt-link-below' }, [ + E('span', { 'class': 'vt-link-rate vt-link-up' }, [ icon('up'), E('b', {}, [ up_r ? fmt.bitsParts(up_r.tx).v : fmt.DASH ]), E('small', {}, [ up_r ? fmt.bitsParts(up_r.tx).u : '' ]) ]), + E('span', { 'class': 'vt-link-meta' }, [ up ? [ up.dev, up.speed ? (up.speed.mbps >= 1000 ? (up.speed.mbps / 1000) + 'G' : up.speed.mbps + 'M') + (up.speed.duplex === 'full' ? ' FD' : up.speed.duplex === 'half' ? ' HD' : '') : null ].filter(Boolean).join(' · ') : _('no uplink') ]) + ]) + ]); + + var d = m.device; + var ap = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ap', 'click': function() { self.openDrawer('device', 'device'); } }, [ + E('span', { 'class': 'vt-node-ico' }, [ icon('ap') ]), + E('span', { 'class': 'vt-node-body' }, [ + E('span', { 'class': 'vt-node-kicker' }, [ _('This access point') ]), + E('span', { 'class': 'vt-node-title' }, [ d.model ]), + E('span', { 'class': 'vt-node-sub vt-mono' }, [ up && up.ipv4[0] ? up.ipv4[0].split('/')[0] : fmt.DASH ]), + E('span', { 'class': 'vt-node-sub' }, [ d.uptime != null ? _('up %s').format(fmt.duration(d.uptime)) : '' ]) + ]) + ]); + this.nodes.ap = ap; + + var branches = E('div', { 'class': 'vt-branches' }); + this.nodes.radios = []; + m.radios.forEach(function(r) { + var rr = st.radioRates[r.id]; + var rnode = E('button', { 'type': 'button', 'class': 'vt-node vt-node-radio' + (r.up ? '' : ' vt-node-off'), 'click': function() { self.openDrawer('radio', r.id); } }, [ + E('span', { 'class': 'vt-node-body' }, [ + E('span', { 'class': 'vt-node-kicker' }, [ bandBadge(r.band), E('span', { 'class': 'vt-node-gen' }, [ r.gen || '' ]) ]), + E('span', { 'class': 'vt-node-title vt-tn' }, [ r.channel ? _('Ch %d · %d MHz').format(r.channel, r.width || 20) : _('Channel —') ]), + E('span', { 'class': 'vt-node-sub vt-tn' }, [ r.disabled ? _('Disabled') : !r.up ? _('Down') : rr ? '↓ ' + fmt.bits(rr.tx) : fmt.DASH ]) + ]) + ]); + var ssidCol = E('div', { 'class': 'vt-branch-ssids' }); + var ssNodes = []; + m.ssids.filter(function(s) { return s.radio === r.id; }).forEach(function(s) { + var dots = E('span', { 'class': 'vt-dots' }); + m.clients.filter(function(c) { return c.ssidId === s.id; }).slice(0, 8).forEach(function(c) { + var dot = E('span', { 'class': 'vt-dot vt-lvl-' + c.level.level, 'title': c.name + ' · ' + fmt.dbm(c.signal) }); + dots.appendChild(dot); + }); + var sn = E('button', { 'type': 'button', 'class': 'vt-node vt-node-ssid' + (s.clients ? '' : ' vt-node-idle'), 'title': _('Show the clients of %s').format(s.ssid), + 'click': function() { self.filterSsid(s.ssid, s.band); } }, [ + E('span', { 'class': 'vt-node-body' }, [ + E('span', { 'class': 'vt-node-title vt-ssid-name' }, [ icon(s.security.tone === 'open' ? 'unlock' : 'lock', 'vt-ico-sm'), E('span', {}, [ s.ssid || _('(hidden)') ]) ]), + E('span', { 'class': 'vt-node-sub' }, [ dots, E('span', { 'class': 'vt-tn' }, [ s.clients === 1 ? _('1 client') : _('%d clients').format(s.clients) ]) ]) + ]) + ]); + ssNodes.push({ el: sn, active: !!(st.ssidRates[s.id] && st.ssidRates[s.id].rx + st.ssidRates[s.id].tx > 50e3) }); + ssidCol.appendChild(sn); + }); + if (!ssNodes.length) ssidCol.appendChild(E('span', { 'class': 'vt-empty-inline' }, [ _('No SSIDs') ])); + self.nodes.radios.push({ el: rnode, ssids: ssNodes, active: !!(rr && rr.rx + rr.tx > 50e3), rate: rr ? rr.rx + rr.tx : 0 }); + branches.appendChild(E('div', { 'class': 'vt-branch' }, [ rnode, ssidCol ])); + }); + if (!m.radios.length) branches.appendChild(E('div', { 'class': 'vt-empty-inline' }, [ _('No radios reported') ])); + + this.nodes.upActive = !!(up_r && up_r.rx + up_r.tx > 50e3); + this.nodes.upRate = up_r ? up_r.rx + up_r.tx : 0; + path.insertBefore(gw, this.el.wires); + path.insertBefore(link, this.el.wires); + path.insertBefore(ap, this.el.wires); + path.insertBefore(branches, this.el.wires); + this.paintKpis(); + }, + + /* four numbers under the path: who is here, what flows, where, and + who is worst off */ + paintKpis: function() { + var self = this, m = this.m, st = this.st, up = st.uplinkRate; + var byBand = {}; + m.clients.forEach(function(c) { byBand[c.band] = (byBand[c.band] || 0) + 1; }); + var bandText = Object.keys(byBand).sort().map(function(b) { return _('%d on %s').format(byBand[b], wifi.bandLabel(b)); }).join(' · '); + var busiest = null; + m.radios.forEach(function(r) { var i = st.radioInsights[r.id]; if (i && i.busiest) busiest = { r: r, share: i.share }; }); + var weakest = m.clients.filter(function(c) { return c.signal != null; }).sort(function(a, b) { return a.signal - b.signal; })[0]; + + function kpi(label, value, sub, tone, onclick) { + var body = [ E('span', { 'class': 'vt-kpi-label' }, [ label ]), E('span', { 'class': 'vt-kpi-value vt-tn' }, [ value ]), E('span', { 'class': 'vt-kpi-sub' }, [ sub ]) ]; + return onclick + ? E('button', { 'type': 'button', 'class': 'vt-kpi vt-kpi-' + (tone || 'plain'), 'click': onclick }, body) + : E('div', { 'class': 'vt-kpi vt-kpi-' + (tone || 'plain') }, body); + } + dom.content(this.el.kpis, [ + kpi(_('Clients'), String(m.clients.length), bandText || _('none connected'), 'plain', function() { self.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); }), + kpi(_('Traffic now'), up ? '↓ ' + fmt.bits(up.rx) : fmt.DASH, up ? _('↑ %s · via %s').format(fmt.bits(up.tx), m.uplink ? m.uplink.dev : '') : _('measuring'), 'plain', function() { self.openDrawer('uplink', 'uplink'); }), + busiest ? kpi(_('Busiest radio'), busiest.r.bandLabel, _('%s of Wi-Fi traffic').format(fmt.pct(busiest.share * 100)), 'plain', function() { self.openDrawer('radio', busiest.r.id); }) + : kpi(_('Busiest radio'), fmt.DASH, _('no Wi-Fi traffic yet'), 'plain'), + weakest ? kpi(_('Weakest signal'), fmt.dbm(weakest.signal), weakest.name, weakest.signal < insight.WEAK_DBM ? 'warn' : 'plain', function() { self.openDrawer('client', weakest.mac); }) + : kpi(_('Weakest signal'), fmt.DASH, _('no clients'), 'plain') + ]); + }, + + scheduleWires: function() { + var self = this; + if (this.wirePending) return; + this.wirePending = true; + window.requestAnimationFrame(function() { self.wirePending = false; self.drawWires(); }); + }, + + /* connectors between the hero nodes, measured after layout; wires are + kept by key so the flow animation does not restart every poll */ + drawWires: function() { + var path = this.el && this.el.path, svg = this.el && this.el.wires, n = this.nodes; + if (!path || !svg || !n || !path.isConnected) return; + var box = path.getBoundingClientRect(); + if (!box.width) return; + svg.setAttribute('viewBox', '0 0 ' + Math.round(box.width) + ' ' + Math.round(box.height)); + svg.setAttribute('width', String(Math.round(box.width))); + svg.setAttribute('height', String(Math.round(box.height))); + + function rect(el) { + var r = el.getBoundingClientRect(); + return { l: r.left - box.left, r: r.right - box.left, t: r.top - box.top, b: r.bottom - box.top, cy: (r.top + r.bottom) / 2 - box.top }; + } + function curve(a, b) { + var x1, y1, x2, y2; + if (b.l >= a.r - 2) { + x1 = a.r; y1 = a.cy; x2 = b.l; y2 = b.cy; + var mx = (x1 + x2) / 2; + return 'M' + x1.toFixed(1) + ' ' + y1.toFixed(1) + 'C' + mx.toFixed(1) + ' ' + y1.toFixed(1) + ' ' + mx.toFixed(1) + ' ' + y2.toFixed(1) + ' ' + x2.toFixed(1) + ' ' + y2.toFixed(1); + } + /* stacked layout: an elbow down the left edge */ + x1 = a.l + 18; y1 = a.b; x2 = b.l; y2 = b.cy; + if (x2 <= x1 + 6) return 'M' + x1.toFixed(1) + ' ' + y1.toFixed(1) + 'V' + b.t.toFixed(1); + var rad = Math.min(8, Math.max(0, y2 - y1)); + return 'M' + x1.toFixed(1) + ' ' + y1.toFixed(1) + 'V' + (y2 - rad).toFixed(1) + 'Q' + x1.toFixed(1) + ' ' + y2.toFixed(1) + ' ' + (x1 + rad).toFixed(1) + ' ' + y2.toFixed(1) + 'H' + x2.toFixed(1); + } + function speed(bps) { return bps > 100e6 ? 3 : bps > 5e6 ? 2 : 1; } + + var want = []; + want.push({ key: 'gw', d: curve(rect(n.gw), rect(n.ap)), active: n.upActive, speed: speed(n.upRate), main: true }); + n.radios.forEach(function(r, i) { + var rr = rect(r.el); + want.push({ key: 'r' + i, d: curve(rect(n.ap), rr), active: r.active, speed: speed(r.rate) }); + r.ssids.forEach(function(s, j) { want.push({ key: 'r' + i + 's' + j, d: curve(rr, rect(s.el)), active: s.active, speed: 1 }); }); + }); + + var seen = {}, map = this.wireMap; + want.forEach(function(w) { + seen[w.key] = true; + var g = map[w.key]; + if (!g) { + g = map[w.key] = { base: svgPath('', 'vt-wire'), flow: svgPath('', 'vt-flow') }; + svg.appendChild(g.base); + svg.appendChild(g.flow); + } + if (g.d !== w.d) { g.base.setAttribute('d', w.d); g.flow.setAttribute('d', w.d); g.d = w.d; } + g.base.setAttribute('class', 'vt-wire' + (w.main ? ' vt-wire-main' : '') + (w.active ? ' vt-wire-on' : '')); + g.flow.setAttribute('class', 'vt-flow vt-flow-' + w.speed + (w.active ? ' vt-flow-on' : '')); + }); + Object.keys(map).forEach(function(k) { + if (seen[k]) return; + svg.removeChild(map[k].base); + svg.removeChild(map[k].flow); + delete map[k]; + }); + }, + + /* ----------------------------------------------------------- system */ + + buildSystem: function() { + var el = this.el; + el.sysUptime = E('span', { 'class': 'vt-hint vt-tn' }); + el.cpuVal = E('span', { 'class': 'vt-big vt-tn' }); + el.cpuCores = E('div', { 'class': 'vt-cores' }); + el.cpuSpark = E('div', { 'class': 'vt-spark-slot' }); + el.cpuLoad = E('span', { 'class': 'vt-hint vt-tn', 'title': _('Load average over 1, 5 and 15 minutes') }); + el.memHead = E('div', { 'class': 'vt-mem-head' }); + el.memBar = E('div', { 'class': 'vt-seg', 'role': 'img' }); + el.memLegend = E('div', { 'class': 'vt-legend' }); + el.swap = E('div', { 'class': 'vt-swap' }); + el.storage = E('div', { 'class': 'vt-storage' }); + el.fw = E('div', { 'class': 'vt-fw' }); + return this.foldable('sys', E('section', { 'class': 'vt-tile vt-sys', 'aria-label': _('System') }, [ + tileHead('cpu', _('System'), [ el.sysUptime, this.foldButton('sys', _('Show or hide system details')) ]), + E('div', { 'class': 'vt-sys-cpu' }, [ + E('div', { 'class': 'vt-sys-cpu-l' }, [ + E('span', { 'class': 'vt-label' }, [ _('CPU') ]), + el.cpuVal, + el.cpuLoad + ]), + E('div', { 'class': 'vt-sys-cpu-r' }, [ + E('div', { 'class': 'vt-spark-box' }, [ el.cpuSpark, E('span', { 'class': 'vt-cap' }, [ _('last 5 min') ]) ]), + E('div', { 'class': 'vt-cores-box', 'title': _('Busy share of each CPU core') }, [ el.cpuCores, E('span', { 'class': 'vt-cap' }, [ _('per core') ]) ]) + ]) + ]), + E('div', { 'class': 'vt-sys-mem' }, [ + E('span', { 'class': 'vt-label' }, [ _('Memory') ]), + el.memHead, el.memBar, el.memLegend, el.swap + ]), + E('div', { 'class': 'vt-sys-store' }, [ E('span', { 'class': 'vt-label' }, [ _('Storage') ]), el.storage ]), + el.fw + ])); + }, + + paintSystem: function() { + var el = this.el, d = this.m.device, cpu = this.st.cpu; + txt(el.sysUptime, d.uptime != null ? _('Up %s').format(fmt.duration(d.uptime)) : ''); + txt(el.cpuVal, cpu ? Math.round(cpu.pct) + '%' : fmt.DASH); + el.cpuVal.className = 'vt-big vt-tn' + (cpu && cpu.pct >= 90 ? ' vt-t-err' : cpu && cpu.pct >= 70 ? ' vt-t-warn' : ''); + txt(el.cpuLoad, _('Load avg %s').format(d.load.map(fmt.load).join(' · '))); + dom.content(el.cpuSpark, [ sparkline(this.st.hist.cpu, [ 1 ], 100, 'vt-spark-cpu', { scale: function() { return '100%'; } }) ]); + dom.content(el.cpuCores, (cpu ? cpu.cores : []).map(function(p, i) { + var fill = E('span', { 'class': 'vt-core-fill' }); + fill.style.height = Math.max(4, Math.min(100, p || 0)).toFixed(1) + '%'; + return E('span', { 'class': 'vt-core', 'title': _('Core %d: %s').format(i, fmt.pct(p)) }, [ fill ]); + })); + + /* memory: one bar, three named segments */ + var mem = d.memory; + if (mem) { + dom.content(el.memHead, [ + E('span', { 'class': 'vt-mem-avail vt-tn' }, [ _('%s available').format(fmt.pct(mem.availPct)) ]), + E('span', { 'class': 'vt-mem-of vt-tn' }, [ _('%s of %s').format(fmt.bytes(mem.available), fmt.bytes(mem.total)) ]) + ]); + if (!el.memSegs) { + el.memSegs = mem.segments.map(function(s) { var x = E('span', { 'class': 'vt-seg-part vt-seg-' + s.key }); el.memBar.appendChild(x); return x; }); + } + mem.segments.forEach(function(s, i) { setW(el.memSegs[i], s.pct); el.memSegs[i].setAttribute('title', s.label + ': ' + fmt.bytes(s.bytes)); }); + el.memBar.setAttribute('aria-label', mem.segments.map(function(s) { return s.label + ' ' + fmt.bytes(s.bytes); }).join(', ')); + dom.content(el.memLegend, mem.segments.map(function(s) { + return E('span', { 'class': 'vt-legend-item' }, [ E('span', { 'class': 'vt-key vt-seg-' + s.key }), E('span', {}, [ s.label ]), E('b', { 'class': 'vt-tn' }, [ fmt.bytes(s.bytes) ]) ]); + })); + } + else { + dom.content(el.memHead, [ E('span', { 'class': 'vt-muted' }, [ _('Not reported') ]) ]); + } + if (d.swap) { + var sb = E('span', { 'class': 'vt-thin-fill' }); + setW(sb, d.swap.usedPct); + dom.content(el.swap, [ E('span', { 'class': 'vt-swap-label' }, [ _('Swap') ]), E('span', { 'class': 'vt-thin' }, [ sb ]), + E('span', { 'class': 'vt-tn vt-hint' }, [ _('%s of %s').format(fmt.bytes(d.swap.used), fmt.bytes(d.swap.total)) ]) ]); + } + else dom.content(el.swap, []); + + dom.content(el.storage, d.storage.map(function(s) { + var f = E('span', { 'class': 'vt-bar-fill' + (s.usedPct >= 95 ? ' vt-bg-err' : s.usedPct >= 85 ? ' vt-bg-warn' : '') }); + setW(f, Math.max(s.usedPct, 0.8)); + return E('div', { 'class': 'vt-mount' }, [ + E('div', { 'class': 'vt-mount-top' }, [ E('span', {}, [ s.label ]), E('span', { 'class': 'vt-tn vt-hint' }, [ _('%s used of %s').format(fmt.bytes(s.used), fmt.bytes(s.total)) ]) ]), + E('div', { 'class': 'vt-bar' }, [ f ]) + ]); + })); + dom.content(el.fw, [ + E('span', {}, [ d.firmware || '' ]), + d.kernel ? E('span', { 'class': 'vt-hint' }, [ _('Kernel %s').format(d.kernel) ]) : '' + ]); + }, + + /* ------------------------------------------------------------ chart */ + + buildChart: function() { + var self = this, el = this.el; + el.chartSub = E('span', { 'class': 'vt-chart-sub vt-tn' }); + el.chartRows = E('div', { 'class': 'vt-mults' }); + el.chartCursor = E('div', { 'class': 'vt-cursor', 'hidden': 'hidden' }); + el.chartTip = E('div', { 'class': 'vt-tip', 'hidden': 'hidden' }); + el.chartOver = E('div', { 'class': 'vt-mult-over', 'aria-hidden': 'true' }, [ el.chartCursor, el.chartTip ]); + el.chartX = E('div', { 'class': 'vt-mult-x', 'aria-hidden': 'true' }); + el.chartBtns = {}; + var seg = E('div', { 'class': 'vt-seg-ctl', 'role': 'group', 'aria-label': _('Direction') }, [ 'down', 'up' ].map(function(k) { + return el.chartBtns[k] = E('button', { 'type': 'button', 'class': 'vt-seg-btn', 'aria-pressed': 'false', + 'click': function() { self.ui.chart = k; self.paintChart(); } }, [ icon(k), E('span', {}, [ k === 'down' ? _('Download') : _('Upload') ]) ]); + })); + var plot = E('div', { 'class': 'vt-mult-wrap', 'tabindex': '0', 'role': 'group', + 'aria-label': _('Throughput per link; arrow keys read the values over time') }, [ el.chartRows, el.chartOver, el.chartX ]); + plot.addEventListener('pointermove', function(ev) { self.chartPointer(ev); }); + plot.addEventListener('pointerleave', function() { if (!self.ui.hover || self.ui.hover.x != null) self.chartHide(); }); + plot.addEventListener('keydown', function(ev) { self.chartKey(ev); }); + plot.addEventListener('blur', function() { self.chartHide(); }); + el.plot = plot; + el.chartLive = E('p', { 'class': 'vt-sr', 'aria-live': 'polite' }); + return this.foldable('chart', E('section', { 'class': 'vt-tile vt-chart', 'aria-label': _('Throughput') }, [ + E('header', { 'class': 'vt-tile-head' }, [ + E('h3', { 'class': 'vt-tile-title' }, [ icon('pulse'), E('span', {}, [ _('Throughput') ]), el.chartSub ]), + E('div', { 'class': 'vt-tile-extra' }, [ seg, this.foldButton('chart', _('Show or hide the throughput chart')) ]) + ]), + plot, + el.chartLive + ])); + }, + + /* one small multiple per link: the uplink, then each radio. Each has + its own y scale (a busy uplink would flatten an idle radio), labelled + with its unit, on a shared time axis and crosshair. */ + chartSeries: function() { + var m = this.m, down = this.ui.chart === 'down'; + var list = [ { key: 'uplink', label: m.uplink ? _('Uplink %s').format(m.uplink.dev) : _('Uplink'), cls: 'vt-c-up', col: down ? 1 : 2 } ]; + m.radios.forEach(function(r) { list.push({ key: r.id, label: _('%s radio').format(r.bandLabel), cls: 'vt-c-' + (r.band || 'x'), col: down ? 2 : 1 }); }); + return list; + }, + + paintChart: function() { + var self = this, el = this.el, net = this.st.hist.net, W = 600, H = 100, now = Date.now(); + var list = this.chartSeries(), down = this.ui.chart === 'down'; + Object.keys(el.chartBtns).forEach(function(k) { el.chartBtns[k].setAttribute('aria-pressed', String(self.ui.chart === k)); }); + + var dm = series.domain(list.map(function(s) { return net[s.key]; }), now, 60000, WINDOW_MS); + var ticks = []; + for (var k = 1; k * 60000 < dm.span - 20000; k++) ticks.push(k); + var rows = [], samples = 0; + this.chartMeta = { t0: dm.t0, t1: dm.t1, rows: rows }; + + dom.content(el.chartRows, list.map(function(s) { + var d = (net[s.key] || []).filter(function(p) { return p[0] >= dm.t0 - POLL_S * 1000; }); + var st = series.stats(d, s.col, dm.t0); + var last = d.length ? d[d.length - 1] : null, fresh = !!(last && now - last[0] < POLL_S * 3000); + var vmax = series.niceCeil(Math.max((st.peak || 0) * 1.05, 1000)); + samples = Math.max(samples, st.n); + + var svg = svgRoot('vt-mult-svg', '0 0 ' + W + ' ' + H); + svg.setAttribute('preserveAspectRatio', 'none'); + svg.appendChild(svgPath('M0 0.5H' + W + 'M0 ' + (H / 2) + 'H' + W, 'vt-chart-grid')); + ticks.forEach(function(k) { + var x = (1 - k * 60000 / dm.span) * W; + svg.appendChild(svgPath('M' + x.toFixed(1) + ' 0V' + H, 'vt-chart-vgrid')); + }); + var segs = series.segments(d, s.col, dm.t0, dm.t1, vmax, W, H, POLL_S * 3500); + var area = svgPath(series.areaPath(segs, H), 'vt-series-area'); + area.setAttribute('fill', fadeFill(svg)); + svg.appendChild(area); + svg.appendChild(svgPath(series.linePath(segs), 'vt-series-line')); + + var tail = segs.length ? segs[segs.length - 1] : null, pt = tail && tail[tail.length - 1]; + var dot = E('span', { 'class': 'vt-mult-dot', 'hidden': 'hidden' }); + rows.push({ s: s, d: d, vmax: vmax, dot: dot }); + return E('div', { 'class': 'vt-mult ' + s.cls }, [ + E('div', { 'class': 'vt-mult-head' }, [ + E('span', { 'class': 'vt-mult-name' }, [ E('span', { 'class': 'vt-lg-key' }), E('span', {}, [ s.label ]) ]), + E('b', { 'class': 'vt-mult-now vt-tn' }, [ fresh && st.now != null ? fmt.bits(last[s.col]) : fmt.DASH ]), + E('span', { 'class': 'vt-mult-stats vt-tn' }, st.n > 1 ? [ + E('span', {}, [ _('peak'), ' ', E('b', {}, [ fmt.bits(st.peak) ]) ]), + E('span', {}, [ _('avg'), ' ', E('b', {}, [ fmt.bits(st.avg) ]) ]) + ] : [ E('span', {}, [ _('measuring…') ]) ]) + ]), + E('div', { 'class': 'vt-mult-plot' }, [ + svg, + st.n ? E('span', { 'class': 'vt-mult-max vt-tn' }, [ bitsLabel(vmax) ]) : '', + st.n < 2 ? E('span', { 'class': 'vt-sk-note' }, [ _('collecting…') ]) : '', + pt && fresh ? pos(E('span', { 'class': 'vt-sk-dot' }), pt[0] / W * 100, pt[1] / H * 100) : '', + dot + ]) + ]); + })); + + /* minute ticks get a label unless it would collide with the ends */ + dom.content(el.chartX, [ E('span', { 'class': 'vt-mx vt-mx-0' }, [ spanAgo(dm.span) ]) ].concat(ticks.filter(function(k) { + var f = 1 - k * 60000 / dm.span; + return f > 0.2 && f < 0.85; + }).map(function(k) { + var l = E('span', { 'class': 'vt-mx vt-mx-t' + (k % 2 ? ' vt-mx-odd' : '') }, [ _('−%d min').format(k) ]); + l.style.left = ((1 - k * 60000 / dm.span) * 100).toFixed(2) + '%'; + return l; + })).concat([ E('span', { 'class': 'vt-mx vt-mx-1' }, [ _('now') ]) ])); + + var age = dm.age; + txt(el.chartSub, (down ? _('to clients') : _('from clients')) + ' · ' + (dm.full ? _('last 5 min') + : samples < 2 ? _('collecting · first samples') : _('collecting · %s of data').format(spanShort(age)))); + el.plot.classList.toggle('vt-collecting', !dm.full); + if (this.ui.hover) this.chartShow(this.ui.hover); + }, + + chartPointer: function(ev) { + var r = this.el.chartOver.getBoundingClientRect(); + if (!this.chartMeta || !r.width || ev.clientX < r.left - 6) { this.chartHide(); return; } + this.chartShow({ x: Math.max(0, Math.min(1, (ev.clientX - r.left) / r.width)) }); + }, + + /* the longest series gives the sample times the crosshair snaps to */ + chartRef: function() { + var ref = []; + (this.chartMeta ? this.chartMeta.rows : []).forEach(function(r) { if (r.d.length > ref.length) ref = r.d; }); + return ref; + }, + + /* at: { x: 0..1 of the plot width } (pointer) or { t: sample time } (keys) */ + chartShow: function(at) { + var meta = this.chartMeta, el = this.el; + if (!meta) return; + var ref = this.chartRef(), span = meta.t1 - meta.t0; + var t = at.t != null ? at.t : meta.t0 + at.x * span; + var i = series.nearest(ref, t); + if (i < 0 || ref[i][0] < meta.t0 - 1000 || (at.x != null && Math.abs(ref[i][0] - t) > POLL_S * 2000)) { this.chartHide(); return; } + var tp = ref[i][0], x = (tp - meta.t0) / span * 100; + this.ui.hover = at.t != null ? { t: tp } : at; + el.chartCursor.hidden = false; + el.chartCursor.style.left = x.toFixed(2) + '%'; + var tipRows = meta.rows.map(function(r) { + var j = series.nearest(r.d, tp), v = (j >= 0 && Math.abs(r.d[j][0] - tp) < POLL_S * 1000) ? r.d[j][r.s.col] : null; + r.dot.hidden = v == null; + if (v != null) pos(r.dot, x, (1 - Math.min(1, v / r.vmax)) * 100); + return E('div', { 'class': 'vt-tip-row ' + r.s.cls }, [ E('span', { 'class': 'vt-lg-key' }), E('b', { 'class': 'vt-tn' }, [ v == null ? fmt.DASH : fmt.bits(v) ]), E('span', { 'class': 'vt-tip-label' }, [ r.s.label ]) ]); + }); + dom.content(el.chartTip, [ E('div', { 'class': 'vt-tip-time vt-tn' }, [ new Date(tp).toLocaleTimeString(), E('span', {}, [ fmt.ago(Date.now() - tp) ]) ]) ].concat(tipRows)); + el.chartTip.hidden = false; + el.chartTip.style.left = x.toFixed(2) + '%'; + el.chartTip.classList.toggle('vt-tip-left', x > 55); + if (at.t != null) txt(el.chartLive, [ new Date(tp).toLocaleTimeString() ].concat(tipRows.map(function(r) { return r.textContent; })).join('; ')); + }, + + chartHide: function() { + var el = this.el; + this.ui.hover = null; + el.chartCursor.hidden = true; + el.chartTip.hidden = true; + (this.chartMeta ? this.chartMeta.rows : []).forEach(function(r) { r.dot.hidden = true; }); + }, + + chartKey: function(ev) { + var ref = this.chartRef(), meta = this.chartMeta; + if (!ref.length || !meta) return; + var cur = this.ui.hover && this.ui.hover.t != null ? series.nearest(ref, this.ui.hover.t) : -1, i = cur; + if (ev.key === 'ArrowLeft') i = cur < 0 ? ref.length - 1 : Math.max(0, cur - 1); + else if (ev.key === 'ArrowRight') i = cur < 0 ? ref.length - 1 : Math.min(ref.length - 1, cur + 1); + else if (ev.key === 'Home') i = 0; + else if (ev.key === 'End') i = ref.length - 1; + else if (ev.key === 'Escape' && this.ui.hover) { ev.stopPropagation(); this.chartHide(); return; } + else return; + ev.preventDefault(); + while (i < ref.length - 1 && ref[i][0] < meta.t0) i++; + this.chartShow({ t: ref[i][0] }); + }, + + /* ----------------------------------------------------------- radios */ + + buildRadios: function() { + var el = this.el; + el.radioCount = E('span', { 'class': 'vt-hint vt-tn' }); + el.radioSum = E('div', { 'class': 'vt-radio-sum vt-fold-keep' }); + el.radioList = E('div', { 'class': 'vt-radio-list' }); + el.radios = E('section', { 'class': 'vt-radios', 'aria-label': _('Radios') }, [ + E('header', { 'class': 'vt-tile-head vt-sec-head' }, [ + E('h3', { 'class': 'vt-tile-title' }, [ icon('radio'), E('span', {}, [ _('Radios') ]) ]), + E('div', { 'class': 'vt-tile-extra' }, [ el.radioCount, this.foldButton('radios', _('Show or hide the radio details')) ]) + ]), + el.radioSum, + el.radioList + ]); + return this.foldable('radios', el.radios); + }, + + paintRadios: function() { + var self = this, m = this.m, st = this.st; + txt(this.el.radioCount, m.radios.length === 1 ? _('1 radio') : _('%d radios').format(m.radios.length)); + /* condensed rows, shown when the section is folded on a phone */ + dom.content(this.el.radioSum, m.radios.map(function(r) { + var rr = st.radioRates[r.id]; + return E('button', { 'type': 'button', 'class': 'vt-rsum', 'click': function() { self.openDrawer('radio', r.id); } }, [ + E('span', { 'class': 'vt-status vt-status-' + (r.disabled ? 'warn' : r.up ? 'ok' : 'err'), 'aria-hidden': 'true' }), + bandBadge(r.band), + E('span', { 'class': 'vt-rsum-main vt-tn' }, [ r.channel ? _('Ch %d · %d MHz').format(r.channel, r.width || 20) : fmt.DASH ]), + E('span', { 'class': 'vt-rsum-sub vt-tn' }, [ r.clients === 1 ? _('1 client') : _('%d clients').format(r.clients) ]), + E('span', { 'class': 'vt-rsum-rate vt-tn' }, [ r.disabled ? _('Disabled') : !r.up ? _('Down') : rr ? '↓ ' + fmt.bits(rr.tx) : fmt.DASH ]), + icon('chevron', 'vt-ico-sm') + ]); + })); + if (!m.radios.length) { + dom.content(this.el.radioList, [ E('div', { 'class': 'vt-tile vt-empty' }, [ icon('radio'), E('p', {}, [ _('No wireless radios are reported by this device.') ]) ]) ]); + return; + } + dom.content(this.el.radioList, m.radios.map(function(r) { + var rr = st.radioRates[r.id], ins = st.radioInsights[r.id] || { notes: [] }; + var sp = r.spectrum, spec = E('div', { 'class': 'vt-spectrum', 'title': sp ? _('%d–%d MHz of the %d–%d MHz band').format(sp.lo, sp.hi, sp.bandLo, sp.bandHi) : '' }); + if (sp) { + var blk = E('span', { 'class': 'vt-spectrum-blk' }); + blk.style.left = (sp.from * 100).toFixed(2) + '%'; + blk.style.width = Math.max(1.2, (sp.to - sp.from) * 100).toFixed(2) + '%'; + spec.appendChild(blk); + spec.appendChild(E('span', { 'class': 'vt-spectrum-lo vt-tn' }, [ String(sp.bandLo) ])); + spec.appendChild(E('span', { 'class': 'vt-spectrum-hi vt-tn' }, [ _('%s MHz').format(sp.bandHi) ])); + } + var hist = st.hist.net[r.id] || []; + var clients = m.clients.filter(function(c) { return c.radio === r.id; }); + return E('article', { 'class': 'vt-tile vt-radio vt-radio-' + (r.band || 'x') + (r.up ? '' : ' vt-radio-off') + (ins.busiest ? ' vt-radio-busiest' : '') }, [ + E('header', { 'class': 'vt-radio-head' }, [ + bandBadge(r.band), + E('span', { 'class': 'vt-radio-name' }, [ r.gen || r.id ]), + E('span', { 'class': 'vt-status vt-status-' + (r.disabled ? 'warn' : r.up ? 'ok' : 'err') }, [ r.disabled ? _('Disabled') : r.up ? _('Up') : _('Down') ]), + E('button', { 'type': 'button', 'class': 'vt-icon-action', 'title': _('Radio details'), 'aria-label': _('Radio details'), + 'click': function() { self.openDrawer('radio', r.id); } }, [ icon('chevron') ]) + ]), + E('div', { 'class': 'vt-radio-chan' }, [ + E('span', { 'class': 'vt-big vt-tn' }, [ r.channel ? _('Ch %d').format(r.channel) : fmt.DASH ]), + E('span', { 'class': 'vt-radio-width vt-tn' }, [ r.width ? _('%s MHz').format(r.width) : '', r.auto ? ' · ' + _('auto') : '' ]) + ]), + spec, + E('dl', { 'class': 'vt-stats' }, [ + kv(_('Clients'), E('span', { 'class': 'vt-tn' }, [ String(r.clients) ])), + kv(_('Tx power'), E('span', { 'class': 'vt-tn' }, [ r.txpower != null ? _('%s dBm').format(r.txpower) : fmt.DASH ])), + kv(_('Noise'), r.noise != null ? E('span', { 'class': 'vt-tn' }, [ fmt.dbm(r.noise) ]) : E('span', { 'class': 'vt-muted', 'title': _('The driver reports a placeholder value, so no noise floor or SNR is shown') }, [ _('not reported') ])), + kv(_('Airtime'), r.airtime != null ? E('span', { 'class': 'vt-tn' }, [ fmt.pct(r.airtime) ]) : E('span', { 'class': 'vt-muted' }, [ _('not reported') ])) + ]), + E('div', { 'class': 'vt-radio-live' }, [ + rateLine(rr ? rr.tx : null, rr ? rr.rx : null), + sparkline(hist, [ 2, 1 ], 0, 'vt-spark-' + (r.band || 'x'), { min: 1000, scale: bitsLabel }) + ]), + E('ul', { 'class': 'vt-notes' }, ins.notes.map(function(n) { + return E('li', { 'class': 'vt-note vt-note-' + n.level }, [ icon(n.level === 'warn' ? 'warn' : n.level === 'err' ? 'err' : 'info', 'vt-ico-sm'), E('span', {}, [ n.text ]) ]); + })), + E('footer', { 'class': 'vt-radio-foot' }, [ + E('span', { 'class': 'vt-avs' }, clients.slice(0, 6).map(function(c) { return avatar(c, true); })), + pageLink(insight.PAGES.wireless, _('Radio settings')) + ]) + ]); + })); + }, + + /* ----------------------------------------------------------- clients */ + + buildClients: function() { + var self = this, el = this.el; + el.filter = E('input', { 'type': 'text', 'class': 'vt-filter-input', 'placeholder': _('Filter by name, address, SSID…'), + 'aria-label': _('Filter clients'), 'autocomplete': 'off', 'spellcheck': 'false', + 'input': function(ev) { self.ui.filter = ev.target.value; self.paintClients(); } }); + el.filterClear = E('button', { 'type': 'button', 'class': 'vt-filter-clear', 'aria-label': _('Clear filter'), 'hidden': 'hidden', + 'click': function() { self.setFilter(''); el.filter.focus(); } }, [ icon('close') ]); + el.chips = E('div', { 'class': 'vt-fchips', 'role': 'group', 'aria-label': _('Quick filters') }); + el.count = E('span', { 'class': 'vt-count vt-tn' }); + el.thead = E('thead'); + el.tbody = E('tbody'); + el.clientsEmpty = E('div', { 'class': 'vt-empty', 'hidden': 'hidden' }); + this.rowMap = new Map(); + el.clients = E('section', { 'class': 'vt-tile vt-clients', 'id': 'vt-clients', 'aria-label': _('Clients') }, [ + tileHead('users', _('Clients'), [ el.count ]), + E('div', { 'class': 'vt-toolbar' }, [ + E('label', { 'class': 'vt-filter' }, [ icon('search'), el.filter, E('kbd', { 'class': 'vt-key-hint' }, [ 'F' ]), el.filterClear ]), + el.chips + ]), + E('div', { 'class': 'vt-table-wrap' }, [ E('table', { 'class': 'vt-ctable' }, [ el.thead, el.tbody ]) ]), + el.clientsEmpty + ]); + return el.clients; + }, + + setFilter: function(v) { + this.ui.filter = v; + this.el.filter.value = v; + this.paintClients(); + }, + + filterSsid: function(ssid, band) { + this.ui.chip = 'all'; + this.setFilter(ssid); + this.el.clients.scrollIntoView({ behavior: reducedMotion() ? 'auto' : 'smooth', block: 'start' }); + }, + + sortValue: function(c, key) { + var r = this.st.staRates[c.mac]; + switch (key) { + case 'name': return String(c.name).toLowerCase(); + case 'signal': return c.signal == null ? -999 : c.signal; + case 'ssid': return (c.ssid + ' ' + c.band).toLowerCase(); + case 'rate': return Math.max(c.tx && c.tx.rate || 0, c.rx && c.rx.rate || 0); + case 'exp': return c.exp ? c.exp.score : -1; + case 'time': return c.connected || 0; + default: return r ? r.rx + r.tx : -1; + } + }, + + matches: function(c) { + var chip = this.ui.chip, q = String(this.ui.filter || '').trim().toLowerCase(); + if (chip === 'weak' && !(c.signal != null && c.signal < insight.WEAK_DBM)) return false; + if (chip === 'new' && !c.isNew) return false; + if (chip === 'active' && !(this.st.staRates[c.mac] && this.st.staRates[c.mac].rx + this.st.staRates[c.mac].tx > 8000)) return false; + if (/^band:/.test(chip) && c.band !== chip.slice(5)) return false; + if (!q) return true; + var hay = [ c.name, c.macLabel, c.mac, c.ssid, c.bandLabel, c.gen, c.vendor, c.wps, c.nameSourceLabel ].concat(c.ips || []).join(' ').toLowerCase(); + return q.split(/\s+/).every(function(w) { return hay.indexOf(w) >= 0; }); + }, + + paintClients: function() { + var self = this, m = this.m, st = this.st, el = this.el, ui = this.ui; + + /* quick filter chips: all / bands present / weak / new / active */ + var chipDefs = [ [ 'all', _('All'), m.clients.length ] ]; + var bands = []; + m.clients.forEach(function(c) { if (c.band && bands.indexOf(c.band) < 0) bands.push(c.band); }); + bands.sort().forEach(function(b) { chipDefs.push([ 'band:' + b, wifi.bandLabel(b), m.clients.filter(function(c) { return c.band === b; }).length ]); }); + chipDefs.push([ 'active', _('Active'), m.clients.filter(function(c) { var r = st.staRates[c.mac]; return r && r.rx + r.tx > 8000; }).length ]); + chipDefs.push([ 'weak', _('Weak signal'), m.clients.filter(function(c) { return c.signal != null && c.signal < insight.WEAK_DBM; }).length ]); + chipDefs.push([ 'new', _('New'), m.clients.filter(function(c) { return c.isNew; }).length ]); + if (!chipDefs.some(function(d) { return d[0] === ui.chip; })) ui.chip = 'all'; + dom.content(el.chips, chipDefs.map(function(d) { + return E('button', { 'type': 'button', 'class': 'vt-fchip' + (ui.chip === d[0] ? ' vt-fchip-on' : '') + (d[0] === 'weak' && d[2] ? ' vt-fchip-warn' : ''), + 'aria-pressed': String(ui.chip === d[0]), 'click': function() { ui.chip = d[0]; self.paintClients(); } }, [ + E('span', {}, [ d[1] ]), E('span', { 'class': 'vt-fchip-n vt-tn' }, [ String(d[2]) ]) + ]); + })); + el.filterClear.hidden = !ui.filter; + + /* header with sort buttons */ + var cols = [ [ 'name', _('Device') ], [ 'signal', _('Signal') ], [ 'ssid', _('Network') ], [ 'rate', _('Link rate') ], [ 'live', _('Live') ], [ 'exp', _('Experience') ], [ 'time', _('Online') ] ]; + dom.content(el.thead, [ E('tr', {}, cols.map(function(c) { + var on = ui.sort === c[0]; + return E('th', { 'class': 'vt-th-' + c[0], 'scope': 'col', 'aria-sort': on ? (ui.dir > 0 ? 'ascending' : 'descending') : 'none' }, [ + E('button', { 'type': 'button', 'class': 'vt-sort' + (on ? ' vt-sort-on' : ''), 'click': function() { + if (ui.sort === c[0]) ui.dir = -ui.dir; else { ui.sort = c[0]; ui.dir = (c[0] === 'name' || c[0] === 'ssid') ? 1 : -1; } + self.paintClients(); + } }, [ E('span', {}, [ c[1] ]), E('span', { 'class': 'vt-sort-ind', 'aria-hidden': 'true' }, [ on ? (ui.dir > 0 ? '↑' : '↓') : '' ]) ]) + ]); + })) ]); + + var list = m.clients.filter(L.bind(this.matches, this)); + list.sort(function(a, b) { + var va = self.sortValue(a, ui.sort), vb = self.sortValue(b, ui.sort); + if (va < vb) return -ui.dir; + if (va > vb) return ui.dir; + return a.mac < b.mac ? -1 : 1; + }); + var gone = (ui.chip === 'all' || ui.chip === 'new') ? st.gone.map(function(g) { return Object.assign({}, g.client, { gone: true, goneAt: g.at }); }).filter(function(c) { + return !ui.filter || self.matches(Object.assign({}, c, { isNew: false })); + }) : []; + if (ui.chip === 'new') gone = []; + + var maxRate = 0; + m.clients.forEach(function(c) { var r = st.staRates[c.mac]; if (r && r.rx + r.tx > maxRate) maxRate = r.rx + r.tx; }); + + var keep = new Set(); + list.concat(gone).forEach(function(c) { + var key = (c.gone ? 'gone:' : '') + c.mac; + keep.add(key); + var tr = self.rowMap.get(key); + if (!tr) { + tr = E('tr', { 'class': 'vt-row vt-row-enter', 'tabindex': '0' }); + tr.addEventListener('click', function() { self.openDrawer('client', tr.getAttribute('data-mac')); }); + tr.addEventListener('keydown', function(ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); self.openDrawer('client', tr.getAttribute('data-mac')); } }); + self.rowMap.set(key, tr); + } + tr.setAttribute('data-mac', c.mac); + tr.classList.toggle('vt-row-gone', !!c.gone); + tr.classList.toggle('vt-row-sel', !!(ui.drawer && ui.drawer.kind === 'client' && ui.drawer.id === c.mac)); + tr.setAttribute('aria-label', _('Open details of %s').format(c.name)); + dom.content(tr, self.clientCells(c, maxRate)); + el.tbody.appendChild(tr); + }); + this.rowMap.forEach(function(tr, key) { + if (keep.has(key)) return; + if (tr.parentNode) tr.parentNode.removeChild(tr); + self.rowMap.delete(key); + }); + window.setTimeout(function() { self.rowMap.forEach(function(tr) { tr.classList.remove('vt-row-enter'); }); }, 600); + + var total = m.clients.length; + txt(el.count, list.length === total ? (total === 1 ? _('1 connected') : _('%d connected').format(total)) : _('%d of %d').format(list.length, total)); + + el.clientsEmpty.hidden = list.length + gone.length > 0; + if (!el.clientsEmpty.hidden) { + dom.content(el.clientsEmpty, !total ? [ + icon('wifi'), E('p', { 'class': 'vt-empty-title' }, [ _('No clients connected') ]), + E('p', {}, [ _('Stations appear here as soon as they associate with one of the SSIDs.') ]) + ] : [ + icon('search'), E('p', { 'class': 'vt-empty-title' }, [ ui.filter ? _('No clients match “%s”').format(fmt.safeText(ui.filter, 40)) : _('No clients in this view') ]), + E('button', { 'type': 'button', 'class': 'vt-btn', 'click': function() { ui.chip = 'all'; self.setFilter(''); } }, [ _('Show all clients') ]) + ]); + } + }, + + clientCells: function(c, maxRate) { + var st = this.st, r = c.gone ? null : st.staRates[c.mac]; + var total = r ? r.rx + r.tx : 0; + var bar = E('span', { 'class': 'vt-mini-fill' }); + setW(bar, maxRate > 0 ? total / maxRate * 100 : 0); + var tags = []; + if (c.gone) tags.push(pill(_('Left %s').format(fmt.ago(Date.now() - c.goneAt)), 'muted')); + else if (c.isNew) tags.push(pill(_('New'), 'accent', _('Connected after you opened this page'))); + if (c.legacyOnRadio && !c.gone) tags.push(pill(c.gen === 'legacy' ? _('Legacy') : c.gen, 'warn', _('Older Wi-Fi generation than the radio; uses more airtime'))); + var exp = c.exp || { score: 0, level: 'muted', grade: fmt.DASH, reason: '' }; + + return [ + E('td', { 'class': 'vt-td-name', 'data-title': _('Device') }, [ + E('div', { 'class': 'vt-who' }, [ + avatar(c), + E('div', { 'class': 'vt-who-text' }, [ + E('div', { 'class': 'vt-who-label', 'title': c.name }, [ c.name ]), + E('div', { 'class': 'vt-who-sub' }, [ E('span', { 'class': 'vt-src vt-src-' + c.nameSource, 'title': _('Name from: %s').format(this.sourceText(c)) }, [ c.nameSourceLabel ]) ].concat(tags, [ E('span', { 'class': 'vt-mono vt-who-id' }, [ c.secondary ]) ])) + ]) + ]) + ]), + E('td', { 'class': 'vt-td-signal', 'data-title': _('Signal') }, c.gone ? [ E('span', { 'class': 'vt-muted' }, [ fmt.DASH ]) ] : [ + E('span', { 'class': 'vt-signal', 'title': c.level.word }, [ signalGlyph(c.level), E('span', { 'class': 'vt-tn' }, [ fmt.dbm(c.signal) ]) ]) + ]), + E('td', { 'class': 'vt-td-net', 'data-title': _('Network') }, [ + E('div', { 'class': 'vt-net' }, [ E('span', { 'class': 'vt-net-ssid' }, [ c.ssid ]), E('span', { 'class': 'vt-net-badges' }, [ bandBadge(c.band), c.gen ? E('span', { 'class': 'vt-gen' }, [ c.gen ]) : '' ]) ]) + ]), + E('td', { 'class': 'vt-td-rate', 'data-title': _('Link rate'), + 'title': _('To device: %s').format(c.tx ? c.tx.label : fmt.DASH) + '\n' + _('From device: %s').format(c.rx ? c.rx.label : fmt.DASH) }, [ + E('div', { 'class': 'vt-phy vt-tn' }, [ icon('down', 'vt-ico-sm'), E('b', {}, [ c.tx ? fmt.phyRate(c.tx.rate) : fmt.DASH ]) ]), + E('div', { 'class': 'vt-phy vt-phy-up vt-tn' }, [ [ c.tx && c.tx.mhz ? _('%s MHz').format(c.tx.mhz) : null, c.tx && c.tx.std ? (c.tx.std === 'legacy' ? _('legacy') : c.tx.std) : null ].filter(Boolean).join(' · ') ]) + ]), + E('td', { 'class': 'vt-td-live', 'data-title': _('Live') }, [ + E('div', { 'class': 'vt-live-cell' }, [ + E('span', { 'class': 'vt-tn vt-live-total' }, [ r ? fmt.bits(total) : fmt.DASH ]), + E('span', { 'class': 'vt-mini' }, [ bar ]), + E('span', { 'class': 'vt-live-split vt-tn', 'title': r ? _('To device %s, from device %s').format(fmt.bits(r.tx), fmt.bits(r.rx)) : '' }, [ r ? '↓ ' + fmt.bitsShort(r.tx) + ' ↑ ' + fmt.bitsShort(r.rx) : '' ]) + ]) + ]), + E('td', { 'class': 'vt-td-exp', 'data-title': _('Experience') }, c.gone ? [ E('span', { 'class': 'vt-muted' }, [ fmt.DASH ]) ] : [ + E('div', { 'class': 'vt-exp', 'title': exp.reason }, [ + E('span', { 'class': 'vt-score vt-score-' + exp.level + ' vt-tn' }, [ String(exp.score) ]), + E('span', { 'class': 'vt-exp-text' }, [ E('b', {}, [ exp.grade ]), E('span', {}, [ exp.short || exp.reason ]) ]) + ]) + ]), + E('td', { 'class': 'vt-td-time vt-tn', 'data-title': _('Connected') }, [ c.gone ? fmt.DASH : fmt.duration(c.connected) ]) + ]; + }, + + /* ----------------------------------------------------- top talkers */ + + buildTalkers: function() { + this.el.talkers = E('div', { 'class': 'vt-talkers' }); + return E('section', { 'class': 'vt-tile vt-talk', 'aria-label': _('Top talkers') }, [ + tileHead('pulse', _('Top talkers'), [ E('span', { 'class': 'vt-hint' }, [ _('who uses the bandwidth') ]) ]), + this.el.talkers + ]); + }, + + paintTalkers: function() { + var self = this, m = this.m, t = this.st.talkers || []; + if (!t.length) { + dom.content(this.el.talkers, [ E('p', { 'class': 'vt-empty-inline' }, [ m.clients.length ? _('Nobody is moving data right now.') : _('No clients connected.') ]) ]); + return; + } + var byMac = {}; + m.clients.forEach(function(c) { byMac[c.mac] = c; }); + dom.content(this.el.talkers, t.map(function(x, i) { + var c = byMac[x.mac] || { mac: x.mac, name: x.name, icon: 'device' }; + var fill = E('span', { 'class': 'vt-talk-fill' }); + setW(fill, x.share * 100); + return E('button', { 'type': 'button', 'class': 'vt-talk-row', 'click': function() { self.openDrawer('client', x.mac); } }, [ + E('span', { 'class': 'vt-talk-rank vt-tn' }, [ String(i + 1) ]), + avatar(c, true), + E('span', { 'class': 'vt-talk-main' }, [ + E('span', { 'class': 'vt-talk-top' }, [ E('span', { 'class': 'vt-talk-name', 'title': c.name }, [ c.name ]) ]), + E('span', { 'class': 'vt-talk-bar' }, [ fill ]), + E('span', { 'class': 'vt-talk-sub vt-tn', 'title': _('To device %s, from device %s').format(fmt.bits(x.down), fmt.bits(x.up)) }, [ + E('b', {}, [ fmt.bits(x.total) ]), E('span', { 'class': 'vt-talk-share' }, [ fmt.pct(x.share * 100) ]), '↓ ' + fmt.bitsShort(x.down) + ' ↑ ' + fmt.bitsShort(x.up) ]) + ]) + ]); + })); + }, + + /* ------------------------------------------------------------ SSIDs */ + + buildSsids: function() { + this.el.ssids = E('div', { 'class': 'vt-ssids' }); + return this.foldable('ssids', E('section', { 'class': 'vt-tile vt-ssid-tile', 'aria-label': _('Wireless networks') }, [ + tileHead('wifi', _('Wireless networks'), [ pageLink(insight.PAGES.wireless, _('Edit')), this.foldButton('ssids', _('Show or hide the wireless networks')) ]), + this.el.ssids + ])); + }, + + paintSsids: function() { + var self = this, m = this.m, st = this.st; + var groups = model.groupSsids(m.ssids); + if (!groups.length) { + dom.content(this.el.ssids, [ E('p', { 'class': 'vt-empty-inline' }, [ _('No wireless networks configured.') ]) ]); + return; + } + dom.content(this.el.ssids, groups.map(function(g) { + var rx = 0, tx = 0, has = false; + g.ids.forEach(function(id) { var r = st.ssidRates[id]; if (r) { rx += r.rx; tx += r.tx; has = true; } }); + var bands = g.bands.map(function(b) { return bandBadge(b); }); + return E('button', { 'type': 'button', 'class': 'vt-ssid' + (g.clients ? '' : ' vt-ssid-idle'), 'title': _('Show the clients of %s').format(g.ssid), + 'click': function() { self.filterSsid(g.ssid); } }, [ + E('span', { 'class': 'vt-ssid-ico' + (g.security.tone === 'open' || g.security.tone === 'weak' ? ' vt-t-warn' : '') }, [ icon(g.security.tone === 'open' ? 'unlock' : 'lock') ]), + E('span', { 'class': 'vt-ssid-main' }, [ + E('span', { 'class': 'vt-ssid-top' }, [ E('span', { 'class': 'vt-ssid-name' }, [ g.ssid || _('(hidden)') ]), g.hidden ? pill(_('Hidden'), 'muted') : '' ]), + E('span', { 'class': 'vt-ssid-meta' }, bands.concat([ E('span', { 'class': 'vt-sec', 'title': g.security.long }, [ g.security.short ]) ])) + ]), + E('span', { 'class': 'vt-ssid-side' }, [ + E('span', { 'class': 'vt-ssid-n vt-tn' }, [ String(g.clients) ]), + E('span', { 'class': 'vt-hint vt-tn' }, [ g.clients ? (has ? '↓ ' + fmt.bits(tx) : fmt.DASH) : _('idle') ]) + ]) + ]); + })); + }, + + /* ----------------------------------------------------------- drawer */ + + buildDrawer: function() { + var self = this, el = this.el; + el.drawerTitle = E('div', { 'class': 'vt-drawer-title', 'id': 'vt-drawer-title' }); + el.drawerBody = E('div', { 'class': 'vt-drawer-body' }); + el.drawerClose = E('button', { 'type': 'button', 'class': 'vt-drawer-close', 'aria-label': _('Close'), 'click': function() { self.closeDrawer(); } }, [ icon('close') ]); + el.drawer = E('aside', { 'class': 'vt-drawer', 'role': 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'vt-drawer-title', 'hidden': 'hidden' }, [ + E('div', { 'class': 'vt-drawer-grip', 'aria-hidden': 'true' }), + E('header', { 'class': 'vt-drawer-head' }, [ el.drawerTitle, el.drawerClose ]), + el.drawerBody + ]); + el.drawerBack = E('div', { 'class': 'vt-drawer-back', 'hidden': 'hidden', 'click': function() { self.closeDrawer(); } }); + return E('div', { 'class': 'vt-drawer-layer' }, [ el.drawerBack, el.drawer ]); + }, + + openDrawer: function(kind, id) { + var el = this.el; + this.ui.drawer = { kind: kind, id: id }; + this.ui.editing = null; + if (!el.drawer.hidden) { this.paintDrawer(); this.paintClients(); return; } + this.lastFocus = document.activeElement; + el.drawer.hidden = false; + el.drawerBack.hidden = false; + this.paintDrawer(); + this.paintClients(); + try { history.replaceState(null, '', '#' + kind + '=' + encodeURIComponent(id)); } catch (e) {} + window.requestAnimationFrame(function() { el.root.classList.add('vt-drawer-open'); el.drawerClose.focus(); }); + }, + + closeDrawer: function() { + var el = this.el, self = this; + if (el.drawer.hidden) return; + this.ui.drawer = null; + this.ui.editing = null; + el.root.classList.remove('vt-drawer-open'); + window.setTimeout(function() { if (!self.ui.drawer) { el.drawer.hidden = true; el.drawerBack.hidden = true; } }, reducedMotion() ? 0 : 260); + try { history.replaceState(null, '', location.pathname + location.search); } catch (e) {} + this.paintClients(); + if (this.lastFocus && this.lastFocus.isConnected) this.lastFocus.focus(); + }, + + /* A deep link names an existing thing or nothing: a client by a + station MAC, a radio this device reports; uplink and device take no + id. Anything else is dropped from the address bar, so a crafted link + cannot put its own text in the drawer title. */ + openFromHash: function() { + var hash = location.hash || '', m = /^#(client|radio|uplink|device)=(.{1,128})$/.exec(hash), id = null; + if (m) { + try { id = decodeURIComponent(m[2]); } catch (e) { id = null; } + if (id == null) { /* malformed escape */ } + else if (m[1] === 'client') id = names.normMac(id); + else if (m[1] === 'radio') id = this.m && this.m.radios.some(function(r) { return r.id === id; }) ? id : null; + else id = m[1]; + } + if (id != null) { this.openDrawer(m[1], id); return; } + if (/^#(client|radio|uplink|device)=/.test(hash)) { + try { history.replaceState(null, '', location.pathname + location.search); } catch (e) {} + } + }, + + paintDrawer: function() { + var d = this.ui.drawer, el = this.el; + if (!d || !el.drawer || el.drawer.hidden) return; + if (this.ui.editing) return; /* do not replace a form being typed in */ + var content; + if (d.kind === 'client') content = this.drawerClient(d.id); + else if (d.kind === 'radio') content = this.drawerRadio(d.id); + else if (d.kind === 'uplink') content = this.drawerUplink(); + else content = this.drawerDevice(); + dom.content(el.drawerTitle, content.title); + dom.content(el.drawerBody, content.body); + }, + + section: function(title, children, cls) { + return E('section', { 'class': 'vt-dsec' + (cls ? ' ' + cls : '') }, [ E('h4', { 'class': 'vt-dsec-title' }, [ title ]) ].concat(children)); + }, + + drawerClient: function(mac) { + var self = this, st = this.st, c = null, gone = null; + this.m.clients.forEach(function(x) { if (x.mac === mac) c = x; }); + if (!c) st.gone.forEach(function(g) { if (g.client.mac === mac) gone = g; }); + if (!c && gone) c = Object.assign({}, gone.client, { gone: true, goneAt: gone.at }); + if (!c) return { + title: [ E('span', { 'class': 'vt-drawer-name' }, [ names.normMac(mac) ? fmt.mac(mac) : _('Unknown device') ]) ], + body: [ E('div', { 'class': 'vt-empty' }, [ icon('wifi'), E('p', { 'class': 'vt-empty-title' }, [ _('Not connected') ]), E('p', {}, [ _('This station is not associated right now.') ]) ]) ] + }; + + var r = c.gone ? null : st.staRates[c.mac], hist = st.clientHist[c.mac] || []; + var exp = c.exp || insight.experience(c); + var radio = this.m.radios.filter(function(x) { return x.id === c.radio; })[0]; + + var title = [ + avatar(c), + E('div', { 'class': 'vt-drawer-titles' }, [ + E('div', { 'class': 'vt-drawer-name', 'id': 'vt-drawer-name' }, [ c.name ]), + E('div', { 'class': 'vt-drawer-sub' }, [ + E('span', { 'class': 'vt-src vt-src-' + c.nameSource }, [ c.nameSourceLabel ]), + E('span', { 'class': 'vt-mono' }, [ c.secondary ]) + ]) + ]) + ]; + + var rename = this.canWrite ? E('button', { 'type': 'button', 'class': 'vt-btn vt-btn-ghost', 'click': function() { self.startRename(c); } }, [ icon('pencil'), E('span', {}, [ c.alias ? _('Rename') : _('Name this device') ]) ]) : ''; + var sigSeries = hist.map(function(p) { return [ p[0], p[1] ]; }).filter(function(p) { return isFinite(p[1]); }); + /* dBm as headroom above -95 dBm: -30 dBm at the top, the -75 dBm + weak-signal line dashed */ + var sigSpark = sparkline(sigSeries.map(function(p) { return [ p[0], Math.max(0, 95 - p[1]) ]; }), [ 1 ], 65, 'vt-spark-' + (c.band || 'x'), + { thresh: 95 + insight.WEAK_DBM, threshLabel: fmt.dbm(insight.WEAK_DBM) }); + var sigVals = sigSeries.map(function(p) { return -p[1]; }); + + var body = [ + c.gone ? E('div', { 'class': 'vt-banner vt-banner-muted' }, [ icon('info'), E('span', {}, [ _('Disconnected %s; shown from the last sample.').format(fmt.ago(Date.now() - c.goneAt)) ]) ]) : '', + E('div', { 'class': 'vt-dactions' }, [ rename, pageLink(insight.PAGES.wireless, _('Wireless settings')) ]), + this.el.renameSlot = E('div', { 'class': 'vt-rename-slot' }), + c.gone ? '' : E('div', { 'class': 'vt-expcard vt-expcard-' + exp.level }, [ + E('div', { 'class': 'vt-ring vt-tn' }, [ this.ring(exp.score, exp.level), E('span', { 'class': 'vt-ring-n' }, [ String(exp.score) ]) ]), + E('div', { 'class': 'vt-expcard-text' }, [ + E('div', { 'class': 'vt-expcard-grade' }, [ _('%s experience').format(exp.grade) ]), + E('div', { 'class': 'vt-expcard-reason' }, [ exp.reason ]), + exp.factors.length > 1 ? E('ul', { 'class': 'vt-factors' }, exp.factors.filter(function(f) { return f.cost > 0; }).map(function(f) { + return E('li', {}, [ E('span', { 'class': 'vt-tn' }, [ '−' + f.cost ]), E('span', {}, [ f.text ]) ]); + })) : '' + ]) + ]), + E('div', { 'class': 'vt-dgrid' }, [ + E('div', { 'class': 'vt-dcard' }, [ + E('div', { 'class': 'vt-dcard-label' }, [ _('Signal') ]), + E('div', { 'class': 'vt-dcard-value' }, [ c.gone ? fmt.DASH : fmt.dbm(c.signal) ]), + E('div', { 'class': 'vt-dcard-sub' }, [ c.gone ? '' : c.level.word + (sigVals.length > 1 ? ' · ' + _('range %s … %s').format(fmt.dbm(Math.min.apply(null, sigVals)), fmt.dbm(Math.max.apply(null, sigVals))) : '') ]), + sigSpark + ]), + E('div', { 'class': 'vt-dcard' }, [ + E('div', { 'class': 'vt-dcard-label' }, [ _('Throughput') ]), + E('div', { 'class': 'vt-dcard-value' }, [ r ? fmt.bits(r.tx + r.rx) : fmt.DASH ]), + E('div', { 'class': 'vt-dcard-sub vt-tn' }, [ r ? '↓ ' + fmt.bits(r.tx) + ' · ↑ ' + fmt.bits(r.rx) : _('measuring') ]), + sparkline(hist, [ 2, 3 ], 0, 'vt-spark-' + (c.band || 'x'), { min: 1000, scale: bitsLabel }) + ]) + ]), + this.section(_('Link'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('To device'), c.tx ? [ c.tx.label, c.tx.detail ? E('small', {}, [ c.tx.detail ]) : '' ] : fmt.DASH), + kv(_('From device'), c.rx ? [ c.rx.label, c.rx.detail ? E('small', {}, [ c.rx.detail ]) : '' ] : fmt.DASH), + kv(_('Generation'), c.gen ? [ c.gen, c.legacyOnRadio ? E('small', { 'class': 'vt-t-warn' }, [ _('older than the radio (%s)').format(radio && radio.gen || '') ]) : '' ] : fmt.DASH), + kv(_('Capabilities'), [ c.wmm ? 'WMM' : null, c.mfp ? _('PMF') : null, c.caps && c.caps.rrm ? '802.11k' : null, c.caps && c.caps.mbo ? 'MBO' : null ].filter(Boolean).join(' · ') || fmt.DASH), + kv(_('Noise / SNR'), c.noise != null && c.signal != null ? fmt.dbm(c.noise) + ' · ' + (c.signal - c.noise) + ' dB' : E('span', { 'class': 'vt-muted' }, [ _('not reported by the driver') ])) + ]) ]), + this.section(_('Connection'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Network'), [ c.ssid, ' ', bandBadge(c.band) ]), + kv(_('Radio'), radio ? _('%s · channel %d · %d MHz').format(radio.id, radio.channel || 0, radio.width || 20) : c.radio), + kv(_('Connected for'), c.gone ? fmt.DASH : fmt.duration(c.connected)), + kv(_('Last activity'), c.inactive == null || c.gone ? fmt.DASH : c.inactive < 1000 ? _('now') : _('%s ago').format(fmt.duration(c.inactive / 1000))), + kv(_('Seen since'), c.firstSeen ? (c.isNew ? _('%s (new this session)').format(new Date(c.firstSeen).toLocaleTimeString()) : _('before this page opened')) : fmt.DASH) + ]) ]), + this.section(_('Traffic'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Downloaded'), fmt.bytes(c.txBytes)), + kv(_('Uploaded'), fmt.bytes(c.rxBytes)), + kv(_('Frames to device'), c.txPackets ? _('%d sent · %s retried · %s failed').format(c.txPackets, fmt.pct(c.txRetries / Math.max(1, c.txPackets) * 100, 1), fmt.pct(c.txFailed / Math.max(1, c.txPackets + c.txFailed) * 100, 1)) : fmt.DASH) + ]) ]), + this.section(_('Identity'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('MAC address'), c.macLabel, true), + kv(_('Address type'), c.isPrivate ? _('Private Wi-Fi address (randomised by the device)') : (c.vendor ? _('Globally unique · %s').format(c.vendor) : _('Globally unique'))), + kv(_('IP addresses'), c.ips && c.ips.length ? c.ips.join('\n') : E('span', { 'class': 'vt-muted' }, [ _('not known to this AP') ]), true), + kv(_('Name from'), this.sourceText(c)), + c.wps && c.nameSource !== 'wps' ? kv(_('WPS device name'), c.wps) : '' + ]) ]) + ]; + return { title: title, body: body }; + }, + + sourceText: function(c) { + switch (c.nameSource) { + case 'alias': return _('Your name for this device'); + case 'dns': return _('Reverse DNS of its address'); + case 'mdns': return _('mDNS announcement'); + case 'dhcp': return _('DHCP hostname'); + case 'wps': return _('The name the device announces (WPS)'); + case 'vendor': return _('Manufacturer of the network adapter'); + case 'private': return _('Nothing better known; the address is randomised'); + } + return _('Nothing better known'); + }, + + ring: function(score, level) { + var s = svgRoot('vt-ring-svg vt-lvlc-' + level, '0 0 36 36'); + var c = 2 * Math.PI * 15.5, f = Math.max(0, Math.min(100, score)) / 100 * c; + var track = document.createElementNS(SVGNS, 'circle'); + track.setAttribute('cx', '18'); track.setAttribute('cy', '18'); track.setAttribute('r', '15.5'); + track.setAttribute('class', 'vt-ring-track'); + var arc = document.createElementNS(SVGNS, 'circle'); + arc.setAttribute('cx', '18'); arc.setAttribute('cy', '18'); arc.setAttribute('r', '15.5'); + arc.setAttribute('class', 'vt-ring-arc'); + arc.setAttribute('stroke-dasharray', f.toFixed(2) + ' ' + c.toFixed(2)); + s.appendChild(track); + s.appendChild(arc); + return s; + }, + + /* inline alias editor; Enter saves, Escape cancels, empty clears */ + startRename: function(c) { + var self = this, slot = this.el.renameSlot, chosen = { icon: (this.aliases[c.mac] && this.aliases[c.mac].icon) || null }; + this.ui.editing = c.mac; + var err = E('p', { 'class': 'vt-form-err', 'role': 'alert', 'hidden': 'hidden' }); + var input = E('input', { 'type': 'text', 'class': 'vt-rename-input', 'maxlength': String(names.NAME_MAX * 2), 'autocomplete': 'off', + 'placeholder': _('e.g. Living room TV'), 'aria-label': _('Device name') }); + input.value = c.alias || ''; + var iconBtns = names.ICONS.map(function(ic) { + return E('button', { 'type': 'button', 'class': 'vt-icon-pick', 'title': ic, 'aria-label': ic, 'aria-pressed': String(chosen.icon === ic), + 'click': function(ev) { + chosen.icon = chosen.icon === ic ? null : ic; + iconBtns.forEach(function(b) { b.setAttribute('aria-pressed', String(b.getAttribute('aria-label') === chosen.icon)); }); + } }, [ icon(ic) ]); + }); + function save() { + var v = names.validateAlias(input.value); + if (!v.ok) { txt(err, v.error); err.hidden = false; input.focus(); return; } + saveBtn.disabled = true; + self.saveAlias(c.mac, v.value, chosen.icon).then(function(ok) { + saveBtn.disabled = false; + if (typeof ok === 'string') { txt(err, ok); err.hidden = false; return; } + if (!ok) { txt(err, _('Could not save the name (no permission or the device refused it).')); err.hidden = false; return; } + self.ui.editing = null; + self.rerender(); + }); + } + function cancel() { self.ui.editing = null; dom.content(slot, []); self.paintDrawer(); } + input.addEventListener('keydown', function(ev) { + if (ev.key === 'Enter') { ev.preventDefault(); save(); } + else if (ev.key === 'Escape') { ev.preventDefault(); ev.stopPropagation(); cancel(); } + }); + var saveBtn = E('button', { 'type': 'button', 'class': 'vt-btn vt-btn-primary', 'click': save }, [ _('Save') ]); + dom.content(slot, [ E('div', { 'class': 'vt-rename' }, [ + E('label', { 'class': 'vt-rename-label' }, [ _('Name'), input ]), + E('div', { 'class': 'vt-icon-picks', 'role': 'group', 'aria-label': _('Device type') }, iconBtns), + err, + E('div', { 'class': 'vt-rename-actions' }, [ + saveBtn, + E('button', { 'type': 'button', 'class': 'vt-btn', 'click': cancel }, [ _('Cancel') ]), + c.alias ? E('button', { 'type': 'button', 'class': 'vt-btn vt-btn-ghost', 'click': function() { input.value = ''; save(); } }, [ _('Remove name') ]) : '', + E('span', { 'class': 'vt-hint' }, [ _('Stored on this device in /etc/config/vantage') ]) + ]) + ]) ]); + input.focus(); + }, + + /* Checked here for a quick answer, then sent to luci.vantage + set_alias, which checks again, keeps one section per MAC and writes + /etc/config/vantage directly (no staged uci changes, so LuCI's + apply/rollback lock does not block it). The names are reloaded after + every attempt, failed or not, so the next one starts from what the + device holds. + -> Promise */ + saveAlias: function(mac, name, iconName) { + var self = this, key = names.normMac(mac); + var op = names.aliasOp(key ? this.aliases[key] || null : null, mac, name, iconName); + if (!op.ok) return Promise.resolve(op.error); + var value = op.op === 'add' || op.op === 'set' ? op.values.name : ''; + var icon = value ? (names.validIcon(iconName) || '') : ''; + return Promise.resolve(callSetAlias(key, value, icon)).then(function(r) { + return (r && typeof r === 'object' && r.ok === true) ? true : self.aliasError(r); + }, function() { return false; }).then(function(res) { + return self.loadAliases().then(function() { return res; }, function() { return res; }); + }); + }, + + /* set_alias refusal -> message; false for anything else (no grant, no + plugin, transport error) */ + aliasError: function(r) { + switch (r && typeof r === 'object' ? r.error : null) { + case 'invalid-mac': return _('Not a station MAC address'); + case 'invalid-name': return _('Name contains invisible or control characters'); + case 'name-too-long': return _('Name is longer than %d characters').format(names.NAME_MAX); + case 'invalid-icon': return _('Unknown device type'); + case 'too-many': return _('This device already stores %d names; remove some first.').format(names.ALIAS_MAX); + case 'failed': return _('The device could not write /etc/config/vantage.'); + } + return false; + }, + + drawerRadio: function(id) { + var self = this, r = this.m.radios.filter(function(x) { return x.id === id; })[0]; + if (!r) return { title: [ E('span', { 'class': 'vt-drawer-name' }, [ _('Unknown radio') ]) ], body: [ E('p', { 'class': 'vt-empty-inline' }, [ _('This radio is not reported any more.') ]) ] }; + var rr = this.st.radioRates[r.id], ins = this.st.radioInsights[r.id] || { notes: [] }; + var clients = this.m.clients.filter(function(c) { return c.radio === r.id; }); + return { + title: [ E('span', { 'class': 'vt-drawer-ico' }, [ icon('radio') ]), E('div', { 'class': 'vt-drawer-titles' }, [ + E('div', { 'class': 'vt-drawer-name' }, [ r.bandLabel + ' · ' + (r.gen || r.id) ]), + E('div', { 'class': 'vt-drawer-sub vt-mono' }, [ r.id + (r.htmode ? ' · ' + r.htmode : '') ]) + ]) ], + body: [ + E('div', { 'class': 'vt-dactions' }, [ pageLink(insight.PAGES.wireless, _('Radio settings')) ]), + E('div', { 'class': 'vt-dgrid' }, [ + E('div', { 'class': 'vt-dcard' }, [ E('div', { 'class': 'vt-dcard-label' }, [ _('Channel') ]), E('div', { 'class': 'vt-dcard-value' }, [ r.channel ? String(r.channel) : fmt.DASH ]), E('div', { 'class': 'vt-dcard-sub' }, [ [ r.width ? _('%s MHz').format(r.width) : null, r.freq ? fmt.mhz(r.freq) : null ].filter(Boolean).join(' · ') ]) ]), + E('div', { 'class': 'vt-dcard' }, [ E('div', { 'class': 'vt-dcard-label' }, [ _('Traffic') ]), E('div', { 'class': 'vt-dcard-value' }, [ rr ? fmt.bits(rr.tx + rr.rx) : fmt.DASH ]), E('div', { 'class': 'vt-dcard-sub vt-tn' }, [ rr ? '↓ ' + fmt.bits(rr.tx) + ' · ↑ ' + fmt.bits(rr.rx) : '' ]), sparkline(this.st.hist.net[r.id] || [], [ 2, 1 ], 0, 'vt-spark-' + (r.band || 'x'), { min: 1000, scale: bitsLabel }) ]) + ]), + ins.notes.length ? E('ul', { 'class': 'vt-notes' }, ins.notes.map(function(n) { return E('li', { 'class': 'vt-note vt-note-' + n.level }, [ icon(n.level === 'warn' ? 'warn' : 'info', 'vt-ico-sm'), E('span', {}, [ n.text ]) ]); })) : '', + this.section(_('Radio'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Status'), r.disabled ? _('Disabled') : r.up ? _('Up') : _('Down')), + kv(_('Mode'), [ r.htmode || fmt.DASH, r.hwmodes ? E('small', {}, [ r.hwmodes ]) : '' ]), + kv(_('Centre frequency'), r.spectrum ? _('%d MHz (%d–%d MHz)').format(r.spectrum.centre, r.spectrum.lo, r.spectrum.hi) : fmt.DASH), + kv(_('Tx power'), r.txpower != null ? _('%s dBm').format(r.txpower) + (r.txpowerCfg != null && r.txpowerCfg !== r.txpower ? ' ' + _('(configured %d dBm, limited by regulatory rules)').format(r.txpowerCfg) : '') : fmt.DASH), + kv(_('Noise floor'), r.noise != null ? fmt.dbm(r.noise) : E('span', { 'class': 'vt-muted' }, [ r.noiseRaw != null ? _('not reported (driver placeholder %s)').format(fmt.dbm(r.noiseRaw)) : _('not reported') ])), + kv(_('Country'), r.country || fmt.DASH) + ]) ]), + this.section(_('Clients on this radio'), clients.length ? [ E('div', { 'class': 'vt-dlist' }, clients.map(function(c) { + return E('button', { 'type': 'button', 'class': 'vt-dlist-row', 'click': function() { self.openDrawer('client', c.mac); } }, [ + avatar(c, true), E('span', { 'class': 'vt-dlist-name' }, [ c.name ]), + E('span', { 'class': 'vt-signal' }, [ signalGlyph(c.level), E('span', { 'class': 'vt-tn' }, [ fmt.dbm(c.signal) ]) ]) + ]); + })) ] : [ E('p', { 'class': 'vt-empty-inline' }, [ _('No clients; an idle radio is fine.') ]) ]) + ] + }; + }, + + drawerUplink: function() { + var up = this.m.uplink, r = this.st.uplinkRate; + if (!up) return { title: [ E('span', { 'class': 'vt-drawer-name' }, [ _('Uplink') ]) ], body: [ E('p', { 'class': 'vt-empty-inline' }, [ _('No interface with a route is up.') ]) ] }; + var s = up.stats || {}; + return { + title: [ E('span', { 'class': 'vt-drawer-ico' }, [ icon('globe') ]), E('div', { 'class': 'vt-drawer-titles' }, [ + E('div', { 'class': 'vt-drawer-name' }, [ _('Uplink · %s').format(up.iface) ]), + E('div', { 'class': 'vt-drawer-sub vt-mono' }, [ up.l3dev + (up.dev !== up.l3dev ? ' → ' + up.dev : '') ]) + ]) ], + body: [ + E('div', { 'class': 'vt-dactions' }, [ pageLink(insight.PAGES.network, _('Interfaces')), pageLink(insight.PAGES.realtime, _('Realtime graphs')) ]), + E('div', { 'class': 'vt-dgrid' }, [ + E('div', { 'class': 'vt-dcard' }, [ E('div', { 'class': 'vt-dcard-label' }, [ _('From network') ]), E('div', { 'class': 'vt-dcard-value' }, [ r ? fmt.bits(r.rx) : fmt.DASH ]), E('div', { 'class': 'vt-dcard-sub' }, [ _('received on %s').format(up.dev) ]) ]), + E('div', { 'class': 'vt-dcard' }, [ E('div', { 'class': 'vt-dcard-label' }, [ _('To network') ]), E('div', { 'class': 'vt-dcard-value' }, [ r ? fmt.bits(r.tx) : fmt.DASH ]), E('div', { 'class': 'vt-dcard-sub' }, [ _('sent on %s').format(up.dev) ]) ]) + ]), + E('div', { 'class': 'vt-dspark' }, [ sparkline(this.st.hist.net.uplink || [], [ 1, 2 ], 0, 'vt-spark-up', { min: 1000, scale: bitsLabel }) ]), + this.section(_('Link'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Carrier'), up.carrier === false ? _('No link') : _('Up')), + kv(_('Speed'), model.speedLabel(up.speed)), + kv(_('Protocol'), up.proto || fmt.DASH), + kv(_('Interface up for'), up.uptime != null ? fmt.duration(up.uptime) : fmt.DASH) + ]) ]), + this.section(_('Addresses'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('IPv4'), up.ipv4.join('\n') || fmt.DASH, true), + kv(_('IPv6'), up.ipv6.join('\n') || fmt.DASH, true), + kv(_('Gateway'), up.gateway || fmt.DASH, true), + kv(_('DNS'), up.dns.join('\n') || fmt.DASH, true) + ]) ]), + this.section(_('Counters since boot'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Received'), fmt.bytes(s.rx_bytes) + ' · ' + _('%d errors').format(+s.rx_errors || 0)), + kv(_('Sent'), fmt.bytes(s.tx_bytes) + ' · ' + _('%d errors').format(+s.tx_errors || 0)), + kv(_('Dropped'), _('%d in · %d out').format(+s.rx_dropped || 0, +s.tx_dropped || 0)) + ]) ]) + ] + }; + }, + + drawerDevice: function() { + var d = this.m.device, cpu = this.st.cpu; + return { + title: [ E('span', { 'class': 'vt-drawer-ico' }, [ icon('ap') ]), E('div', { 'class': 'vt-drawer-titles' }, [ + E('div', { 'class': 'vt-drawer-name' }, [ d.model ]), + E('div', { 'class': 'vt-drawer-sub' }, [ d.hostname || '' ]) + ]) ], + body: [ + E('div', { 'class': 'vt-dactions' }, [ pageLink([ 'admin', 'system', 'system' ], _('System settings')), pageLink(insight.PAGES.processes, _('Processes')) ]), + this.section(_('Device'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('Model'), d.model), + kv(_('Platform'), [ d.target || fmt.DASH, d.system ? E('small', {}, [ d.system ]) : '' ]), + kv(_('Firmware'), d.firmware || fmt.DASH), + kv(_('Kernel'), d.kernel || fmt.DASH), + kv(_('Uptime'), d.uptime != null ? fmt.duration(d.uptime) : fmt.DASH), + kv(_('Local time'), d.localtime != null ? new Date(d.localtime * 1000).toISOString().replace('T', ' ').replace(/\.\d+Z$/, '') : fmt.DASH, true) + ]) ]), + this.section(_('Load'), [ E('dl', { 'class': 'vt-kvs' }, [ + kv(_('CPU'), cpu ? _('%s over %d cores').format(fmt.pct(cpu.pct), cpu.cores.length || 1) : fmt.DASH), + kv(_('Load average'), d.load.map(fmt.load).join(' · ')), + kv(_('Memory in use'), d.memory ? _('%s of %s').format(fmt.bytes(d.memory.used), fmt.bytes(d.memory.total)) : fmt.DASH) + ]) ]) + ] + }; + }, + + /* ---------------------------------------------------------- keyboard */ + + handleKey: function(ev) { + var t = ev.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)); + if (ev.key === 'Escape' && this.ui.drawer && !document.body.classList.contains('modal-overlay-active')) { + this.closeDrawer(); + return; + } + /* 'f' focuses the client filter; '/' and Ctrl+K belong to the theme */ + if ((ev.key === 'f' || ev.key === 'F') && !typing && !ev.ctrlKey && !ev.metaKey && !ev.altKey && !this.ui.drawer) { + ev.preventDefault(); + this.el.filter.focus(); + this.el.filter.select(); + } + if (ev.key === 'Escape' && t === this.el.filter && this.ui.filter) this.setFilter(''); + } +}); diff --git a/luci-app-vantage/po/templates/vantage.pot b/luci-app-vantage/po/templates/vantage.pot new file mode 100644 index 000000000..fbe7eee86 --- /dev/null +++ b/luci-app-vantage/po/templates/vantage.pot @@ -0,0 +1,1740 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1874 +msgid "%d MHz (%d–%d MHz)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:93 +msgid "%d SSIDs broadcasting" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:93 +msgid "%d SSIDs broadcasting; %d without clients, which is fine" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:92 +msgid "%d SSIDs on air" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:92 +msgid "%d SSIDs · %d idle" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:731 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:843 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1250 +msgid "%d clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +msgid "%d connected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:87 +msgid "%d down" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1918 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1919 +msgid "%d errors" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:91 +msgid "%d h ago" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 +msgid "%d in · %d out" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:294 +msgid "%d min" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:90 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:288 +msgid "%d min ago" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +msgid "%d of %d" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:89 +msgid "%d off" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:236 +msgid "%d older devices (Wi-Fi 4 or earlier) slow airtime" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:870 +msgid "%d on %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 +msgid "%d radios" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:294 +msgid "%d s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:89 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:288 +msgid "%d s ago" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1732 +msgid "%d sent · %s retried · %s failed" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 +msgid "%d things need attention" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:91 +msgid "%d up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:100 +msgid "%d weak" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1261 +msgid "%d–%d MHz of the %d–%d MHz band" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:90 +msgid "%s (%s) is disabled" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:88 +msgid "%s (%s) is enabled but not up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1727 +msgid "%s (new this session)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:25 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:168 +msgid "%s Gbit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:131 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1282 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1486 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +msgid "%s MHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:25 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:168 +msgid "%s Mbit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1726 +msgid "%s ago" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:102 +msgid "%s at %s; below %s roaming and throughput suffer" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:77 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1011 +msgid "%s available" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1287 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1875 +msgid "%s dBm" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:267 +msgid "%s device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:152 +msgid "%s device on a newer radio; uses more airtime per byte" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1694 +msgid "%s experience" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:109 +msgid "%s free" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:109 +msgid "%s free on the overlay (%s used)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:47 +msgid "%s has no carrier: cable unplugged or switch port down" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:89 +msgid "%s is disabled" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:87 +msgid "%s is not up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:55 +msgid "%s link" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:49 +msgid "%s negotiated half duplex; check the cable and switch port" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:51 +msgid "%s negotiated only %d Mbit/s; a cable pair or port may be faulty" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:78 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1012 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1030 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1946 +msgid "%s of %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:79 +msgid "%s of %s available (%s in use)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:884 +msgid "%s of Wi-Fi traffic" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:147 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:148 +msgid "%s of frames fail" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:148 +msgid "%s of frames to the device fail" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:147 +msgid "%s of frames to the device fail; interference likely" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1944 +msgid "%s over %d cores" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1087 +msgid "%s radio" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 +msgid "%s up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 +msgid "%s up at %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:108 +msgid "%s used" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1038 +msgid "%s used of %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:101 +msgid "%s · %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 +msgid "%s · channel %d · %d MHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 +msgid "%s · check cable" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 +msgid "%s · slow link" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 +msgid "%s: no carrier" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1875 +msgid "(configured %d dBm, limited by regulatory rules)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:842 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1565 +msgid "(hidden)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:731 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:843 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1250 +msgid "1 client" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +msgid "1 connected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:236 +msgid "1 older device (Wi-Fi 4 or earlier) slows airtime" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 +msgid "1 radio" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 +msgid "1 thing needs attention" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:12 +msgid "2.4 GHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:13 +msgid "5 GHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:14 +msgid "6 GHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:15 +msgid "60 GHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1377 +msgid "Active" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Address type" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1911 +msgid "Addresses" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1289 +msgid "Airtime" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "Alias" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1373 +msgid "All" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:104 +msgid "All %d clients above %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:103 +msgid "All above %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:103 +msgid "All good" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "B" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:884 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:885 +msgid "Busiest radio" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:235 +msgid "Busiest radio: %s of Wi-Fi traffic" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:982 +msgid "Busy share of each CPU core" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:69 +msgid "Busy · load %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:70 +msgid "Busy; load %s on %d cores; see the busiest processes" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:67 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:68 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:976 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1944 +msgid "CPU" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:80 +msgid "Cache (reclaimable)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1812 +msgid "Cancel" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1719 +msgid "Capabilities" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1906 +msgid "Carrier" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1874 +msgid "Centre frequency" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1281 +msgid "Ch %d" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:827 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1249 +msgid "Ch %d · %d MHz" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +msgid "Channel" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:827 +msgid "Channel —" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1313 +msgid "Clear filter" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:775 +msgid "Click any node for details" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:882 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1286 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +msgid "Clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1879 +msgid "Clients on this radio" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1582 +msgid "Close" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1501 +msgid "Connected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1463 +msgid "Connected after you opened this page" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1725 +msgid "Connected for" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1722 +msgid "Connection" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1004 +msgid "Core %d: %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1795 +msgid "Could not save the name (no permission or the device refused it)." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1917 +msgid "Counters since boot" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1877 +msgid "Country" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "DHCP" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1750 +msgid "DHCP hostname" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +msgid "DNS" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:795 +msgid "DNS %s" +msgstr "" + +#: luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json:3 +msgid "Dashboard" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1468 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 +msgid "Device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1779 +msgid "Device name" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1808 +msgid "Device type" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1059 +msgid "Direction" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:828 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1251 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +msgid "Disabled" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1688 +msgid "Disconnected %s; shown from the last sample." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:44 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:828 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1251 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +msgid "Down" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1061 +msgid "Download" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:256 +msgid "Download (to clients)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1730 +msgid "Downloaded" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 +msgid "Dropped" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 +msgid "Edit" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:25 +msgid "Enhanced Open (OWE)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 +msgid "Everything looks healthy" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:117 +msgid "Excellent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 +msgid "Experience" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:119 +msgid "Fair" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:136 +msgid "Fair signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:136 +msgid "Fair signal %s; lower rates at range" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1310 +msgid "Filter by name, address, SSID…" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1311 +msgid "Filter clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1938 +msgid "Firmware" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1732 +msgid "Frames to device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:81 +msgid "Free" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1717 +msgid "From device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1484 +msgid "From device: %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1901 +msgid "From network" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:793 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1914 +msgid "Gateway" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "Gbit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1718 +msgid "Generation" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "GiB" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Globally unique" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Globally unique · %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:118 +msgid "Good" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:137 +msgid "Good signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:137 +msgid "Good signal %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 +msgid "Half duplex" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:633 +msgid "Health checks" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1565 +msgid "Hidden" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 +msgid "IP addresses" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1912 +msgid "IPv4" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1913 +msgid "IPv6" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1734 +msgid "Identity" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:79 +msgid "In use" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1909 +msgid "Interface up for" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1899 +msgid "Interfaces" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:793 +msgid "Internet" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1939 +msgid "Kernel" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1044 +msgid "Kernel %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "KiB" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1726 +msgid "Last activity" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1462 +msgid "Left %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1464 +msgid "Legacy" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1905 +msgid "Link" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1483 +msgid "Link rate" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:739 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1488 +msgid "Live" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1943 +msgid "Load" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:70 +msgid "Load %s on %d cores" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:69 +msgid "Load %s · %d cores" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1945 +msgid "Load average" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:965 +msgid "Load average over 1, 5 and 15 minutes" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:999 +msgid "Load avg %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1941 +msgid "Local time" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:142 +msgid "Low link rate %s despite good signal; interference or an old device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:142 +msgid "Low link rate for its signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 +msgid "MAC" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1735 +msgid "MAC address" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:106 +msgid "MCS %d" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1752 +msgid "Manufacturer of the network adapter" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "Mbit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:67 +msgid "Measuring over the next poll" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:67 +msgid "Measuring…" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:77 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:986 +msgid "Memory" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1946 +msgid "Memory in use" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "MiB" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1873 +msgid "Mode" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1936 +msgid "Model" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1807 +msgid "Name" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:134 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1845 +msgid "Name contains invisible or control characters" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1738 +msgid "Name from" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1473 +msgid "Name from: %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:132 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:137 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1846 +msgid "Name is longer than %d characters" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:131 +msgid "Name must be text" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1679 +msgid "Name this device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1480 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +msgid "Network" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:774 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:775 +msgid "Network path" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1463 +msgid "New" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:849 +msgid "No SSIDs" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +msgid "No clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1447 +msgid "No clients connected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1518 +msgid "No clients connected." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1450 +msgid "No clients in this view" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1450 +msgid "No clients match “%s”" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1884 +msgid "No clients; an idle radio is fine." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:238 +msgid "No clients; idle radios are fine" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:794 +msgid "No default route" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:44 +msgid "No interface with a route" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1891 +msgid "No interface with a route is up." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1906 +msgid "No link" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:45 +msgid "No network interface with a route is up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:161 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 +msgid "No problems detected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:853 +msgid "No radios reported" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:116 +msgid "No signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +msgid "No stations are associated right now" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1554 +msgid "No wireless networks configured." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1256 +msgid "No wireless radios are reported by this device." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +msgid "Nobody associated" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1518 +msgid "Nobody is moving data right now." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1288 +msgid "Noise" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1720 +msgid "Noise / SNR" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1876 +msgid "Noise floor" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:237 +msgid "Noise floor not reported by the driver" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +msgid "None" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:150 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 +msgid "Not a station MAC address" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 +msgid "Not connected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1024 +msgid "Not reported" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 +msgid "Nothing better known" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1753 +msgid "Nothing better known; the address is randomised" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:25 +msgid "OWE" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:153 +msgid "Older %s device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1464 +msgid "Older Wi-Fi generation than the radio; uses more airtime" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +msgid "Online" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:24 +msgid "Open" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1430 +msgid "Open details of %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:24 +msgid "Open network" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:451 +msgid "OpenWrt device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1719 +msgid "PMF" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:737 +msgid "Paused while hidden" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "PiB" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1937 +msgid "Platform" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +msgid "Poor" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 +msgid "Private" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Private Wi-Fi address (randomised by the device)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:266 +msgid "Private device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1934 +msgid "Processes" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1908 +msgid "Protocol" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1315 +msgid "Quick filters" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1871 +msgid "Radio" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1277 +msgid "Radio details" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:233 +msgid "Radio is disabled" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:234 +msgid "Radio is enabled but not up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1865 +msgid "Radio settings" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1229 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1231 +msgid "Radios" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:795 +msgid "Reachability not probed" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1899 +msgid "Realtime graphs" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1918 +msgid "Received" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:738 +msgid "Reconnecting…" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1813 +msgid "Remove name" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1679 +msgid "Rename" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1748 +msgid "Reverse DNS of its address" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:463 +msgid "Root (overlay)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1805 +msgid "Save" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1727 +msgid "Seen since" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1919 +msgid "Sent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1451 +msgid "Show all clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:973 +msgid "Show or hide system details" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:775 +msgid "Show or hide the network path" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1232 +msgid "Show or hide the radio details" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1074 +msgid "Show or hide the throughput chart" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 +msgid "Show or hide the wireless networks" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1561 +msgid "Show the clients of %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:100 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1477 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1703 +msgid "Signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:133 +msgid "Signal not reported" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1907 +msgid "Speed" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1448 +msgid "Stations appear here as soon as they associate with one of the SSIDs." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +msgid "Status" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:108 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:989 +msgid "Storage" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1814 +msgid "Stored on this device in /etc/config/vantage" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 +msgid "Strong signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:161 +msgid "Strong signal %s, %s link" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 +msgid "Strong signal, %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1029 +msgid "Swap" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:972 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:973 +msgid "System" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1934 +msgid "System settings" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "Tbit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:463 +msgid "Temporary (RAM)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1849 +msgid "The device could not write /etc/config/vantage." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1288 +msgid "" +"The driver reports a placeholder value, so no noise floor or SNR is shown" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1751 +msgid "The name the device announces (WPS)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:156 +msgid "The stored name has an invalid section id" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:812 +msgid "This access point" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1848 +msgid "This device already stores %d names; remove some first." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +msgid "This device reports no radios" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1856 +msgid "This radio is not reported any more." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 +msgid "This station is not associated right now." +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1071 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1073 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1709 +msgid "Throughput" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1064 +msgid "Throughput per link; arrow keys read the values over time" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "TiB" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1716 +msgid "To device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1492 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1533 +msgid "To device %s, from device %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1484 +msgid "To device: %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1902 +msgid "To network" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1509 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1510 +msgid "Top talkers" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1729 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1868 +msgid "Traffic" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:883 +msgid "Traffic now" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1287 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1875 +msgid "Tx power" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +msgid "Unknown device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1847 +msgid "Unknown device type" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1856 +msgid "Unknown radio" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:56 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1906 +msgid "Up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:996 +msgid "Up %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:44 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:56 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1086 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1891 +msgid "Uplink" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1086 +msgid "Uplink %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1895 +msgid "Uplink · %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1061 +msgid "Upload" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:257 +msgid "Upload (from clients)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1731 +msgid "Uploaded" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1940 +msgid "Uptime" +msgstr "" + +#: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:24 +msgid "" +"Vantage dashboard: client names from reverse DNS (lets the holder make the " +"device send DNS PTR queries to any DNS server and port it names)" +msgstr "" + +#: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:32 +msgid "" +"Vantage dashboard: name stations (luci.vantage set_alias: validated names " +"in /etc/config/vantage only, at most 512)" +msgstr "" + +#: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:3 +msgid "" +"Vantage dashboard: read-only status of the device, uplink, radios and " +"stations (wireless configuration without keys, through luci.vantage wireless)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 +msgid "Vendor" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:134 +msgid "Very weak signal %s; expect drops" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:134 +msgid "Very weak signal; expect drops" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:36 +msgid "WEP" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:36 +msgid "WEP (insecure)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:30 +msgid "WPA" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:35 +msgid "WPA-Ent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:35 +msgid "WPA-Enterprise (legacy)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:30 +msgid "WPA-Personal (legacy)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:28 +msgid "WPA/WPA2" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:33 +msgid "WPA/WPA2-Ent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:33 +msgid "WPA/WPA2-Enterprise" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:28 +msgid "WPA/WPA2-Personal (TKIP allowed)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:29 +msgid "WPA2" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:34 +msgid "WPA2-Ent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:34 +msgid "WPA2-Enterprise" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:29 +msgid "WPA2-Personal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:26 +msgid "WPA2/3" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:31 +msgid "WPA2/3-Ent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:31 +msgid "WPA2/WPA3-Enterprise" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:26 +msgid "WPA2/WPA3-Personal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:27 +msgid "WPA3" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:32 +msgid "WPA3-Ent" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:32 +msgid "WPA3-Enterprise" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:27 +msgid "WPA3-Personal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "WPS" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1739 +msgid "WPS device name" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:120 +msgid "Weak" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:135 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1378 +msgid "Weak signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:135 +msgid "Weak signal %s; may roam poorly" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:886 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:887 +msgid "Weakest signal" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:87 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:89 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:91 +msgid "Wi-Fi" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1544 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 +msgid "Wireless networks" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1689 +msgid "Wireless settings" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1747 +msgid "Your name for this device" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1282 +msgid "auto" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1131 +msgid "avg" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1727 +msgid "before this page opened" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "bit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1156 +msgid "collecting · %s of data" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1156 +msgid "collecting · first samples" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:197 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1137 +msgid "collecting…" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:97 +msgid "dBm" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1779 +msgid "e.g. Living room TV" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 +msgid "from clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:169 +msgid "full duplex" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:59 +msgid "gateway %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:169 +msgid "half duplex" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +msgid "idle" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:88 +msgid "just now" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "kbit/s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:981 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 +msgid "last 5 min" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:153 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1486 +msgid "legacy" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "mDNS" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 +msgid "mDNS announcement" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:883 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1711 +msgid "measuring" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1132 +msgid "measuring…" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:885 +msgid "no Wi-Fi traffic yet" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:887 +msgid "no clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:804 +msgid "no uplink" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:882 +msgid "none connected" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 +msgid "not known to this AP" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1288 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1289 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1876 +msgid "not reported" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1876 +msgid "not reported (driver placeholder %s)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1720 +msgid "not reported by the driver" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1726 +msgid "now" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:60 +msgid "now %s down, %s up" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1718 +msgid "older than the radio (%s)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1130 +msgid "peak" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:982 +msgid "per core" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1705 +msgid "range %s … %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1901 +msgid "received on %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1902 +msgid "sent on %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:108 +msgid "short GI" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 +msgid "to clients" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:815 +msgid "up %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1510 +msgid "who uses the bandwidth" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:883 +msgid "↑ %s · via %s" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1149 +msgid "−%d min" +msgstr "" diff --git a/luci-app-vantage/po/zh-cn b/luci-app-vantage/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-app-vantage/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-vantage/po/zh_Hans/vantage.po b/luci-app-vantage/po/zh_Hans/vantage.po new file mode 100644 index 000000000..6aee847eb --- /dev/null +++ b/luci-app-vantage/po/zh_Hans/vantage.po @@ -0,0 +1,1767 @@ +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-09-29 20:29+0800\n" +"Project-Id-Version: luci-app-vantage\n" +"PO-Revision-Date: 2026-09-29 20:30+0800\n" +"Last-Translator: Automatically generated\n" +"Language-Team: Chinese (Simplified) <\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" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1874 +msgid "%d MHz (%d–%d MHz)" +msgstr "%d MHz(%d–%d MHz)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:93 +msgid "%d SSIDs broadcasting" +msgstr "正在广播 %d 个 SSID" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:93 +msgid "%d SSIDs broadcasting; %d without clients, which is fine" +msgstr "正在广播 %d 个 SSID,%d 个无客户端(正常)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:92 +msgid "%d SSIDs on air" +msgstr "%d 个 SSID 正在广播" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:92 +msgid "%d SSIDs · %d idle" +msgstr "%d 个 SSID · %d 个空闲" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:731 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:843 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1250 +msgid "%d clients" +msgstr "%d 个客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +msgid "%d connected" +msgstr "已连接 %d 个" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:87 +msgid "%d down" +msgstr "%d 个已关闭" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1918 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1919 +msgid "%d errors" +msgstr "%d 个错误" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:91 +msgid "%d h ago" +msgstr "%d 小时前" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 +msgid "%d in · %d out" +msgstr "%d 入 · %d 出" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:294 +msgid "%d min" +msgstr "%d 分" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:90 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:288 +msgid "%d min ago" +msgstr "%d 分钟前" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +msgid "%d of %d" +msgstr "%d/%d" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:89 +msgid "%d off" +msgstr "%d 个关闭" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:236 +msgid "%d older devices (Wi-Fi 4 or earlier) slow airtime" +msgstr "%d 台旧设备(Wi-Fi 4 或更早)拖慢空口时间" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:870 +msgid "%d on %s" +msgstr "%d 个在 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 +msgid "%d radios" +msgstr "%d 个射频" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:294 +msgid "%d s" +msgstr "%d 秒" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:89 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:288 +msgid "%d s ago" +msgstr "%d 秒前" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1732 +msgid "%d sent · %s retried · %s failed" +msgstr "发送 %d · 重传 %s · 失败 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 +msgid "%d things need attention" +msgstr "%d 项需要关注" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:91 +msgid "%d up" +msgstr "%d 个已启用" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:100 +msgid "%d weak" +msgstr "%d 个信号弱" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1261 +msgid "%d–%d MHz of the %d–%d MHz band" +msgstr "%d–%d MHz 属于 %d–%d MHz 频段" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:90 +msgid "%s (%s) is disabled" +msgstr "%s(%s)已禁用" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:88 +msgid "%s (%s) is enabled but not up" +msgstr "%s(%s)已启用但未上线" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1727 +msgid "%s (new this session)" +msgstr "%s(本次新增)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:25 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:168 +msgid "%s Gbit/s" +msgstr "%s Gbit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:131 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1282 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1486 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +msgid "%s MHz" +msgstr "%s MHz" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:25 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:168 +msgid "%s Mbit/s" +msgstr "%s Mbit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1726 +msgid "%s ago" +msgstr "%s 前" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:102 +msgid "%s at %s; below %s roaming and throughput suffer" +msgstr "%s(%s);低于 %s 时漫游和吞吐量会受影响" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:77 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1011 +msgid "%s available" +msgstr "可用 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1287 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1875 +msgid "%s dBm" +msgstr "%s dBm" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:267 +msgid "%s device" +msgstr "%s 设备" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:152 +msgid "%s device on a newer radio; uses more airtime per byte" +msgstr "%s 设备连接在较新的射频上,每字节占用更多空口时间" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1694 +msgid "%s experience" +msgstr "%s 体验" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:109 +msgid "%s free" +msgstr "空闲 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:109 +msgid "%s free on the overlay (%s used)" +msgstr "overlay 空闲 %s(已用 %s)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:47 +msgid "%s has no carrier: cable unplugged or switch port down" +msgstr "%s 无载波:网线未插或交换机端口关闭" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:89 +msgid "%s is disabled" +msgstr "%s 已禁用" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:87 +msgid "%s is not up" +msgstr "%s 未上线" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:55 +msgid "%s link" +msgstr "%s 链路" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:49 +msgid "%s negotiated half duplex; check the cable and switch port" +msgstr "%s 协商为半双工;请检查网线和交换机端口" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:51 +msgid "%s negotiated only %d Mbit/s; a cable pair or port may be faulty" +msgstr "%s 仅协商到 %d Mbit/s;网线对线或端口可能故障" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:78 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1012 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1030 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1946 +msgid "%s of %s" +msgstr "%s / %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:79 +msgid "%s of %s available (%s in use)" +msgstr "可用 %s / 共 %s(已用 %s)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:884 +msgid "%s of Wi-Fi traffic" +msgstr "占 Wi-Fi 流量的 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:147 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:148 +msgid "%s of frames fail" +msgstr "帧失败率:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:148 +msgid "%s of frames to the device fail" +msgstr "发往设备的帧失败率:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:147 +msgid "%s of frames to the device fail; interference likely" +msgstr "发往设备的帧失败率:%s,可能存在干扰" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1944 +msgid "%s over %d cores" +msgstr "%s / %d 核" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1087 +msgid "%s radio" +msgstr "%s 射频" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 +msgid "%s up" +msgstr "%s 已连接" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:58 +msgid "%s up at %s" +msgstr "%s 协商速率为 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:108 +msgid "%s used" +msgstr "已用 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1038 +msgid "%s used of %s" +msgstr "已用 %s / 共 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:101 +msgid "%s · %s" +msgstr "%s · %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 +msgid "%s · channel %d · %d MHz" +msgstr "%s · 信道 %d · %d MHz" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 +msgid "%s · check cable" +msgstr "%s · 检查网线" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 +msgid "%s · slow link" +msgstr "%s · 链路较慢" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 +msgid "%s: no carrier" +msgstr "%s:无载波" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1875 +msgid "(configured %d dBm, limited by regulatory rules)" +msgstr "(配置 %d dBm,受监管规则限制)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:842 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1565 +msgid "(hidden)" +msgstr "(隐藏)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:731 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:843 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1250 +msgid "1 client" +msgstr "1 个客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1442 +msgid "1 connected" +msgstr "已连接 1 个" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:236 +msgid "1 older device (Wi-Fi 4 or earlier) slows airtime" +msgstr "1 台旧设备(Wi-Fi 4 或更早)拖慢空口时间" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1242 +msgid "1 radio" +msgstr "1 个射频" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 +msgid "1 thing needs attention" +msgstr "1 项需要关注" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:12 +msgid "2.4 GHz" +msgstr "2.4 GHz" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:13 +msgid "5 GHz" +msgstr "5 GHz" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:14 +msgid "6 GHz" +msgstr "6 GHz" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:15 +msgid "60 GHz" +msgstr "60 GHz" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1377 +msgid "Active" +msgstr "活跃" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Address type" +msgstr "地址类型" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1911 +msgid "Addresses" +msgstr "地址" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1289 +msgid "Airtime" +msgstr "空口时间" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "Alias" +msgstr "别名" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1373 +msgid "All" +msgstr "全部" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:104 +msgid "All %d clients above %s" +msgstr "全部 %d 个客户端高于 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:103 +msgid "All above %s" +msgstr "全部高于 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:103 +msgid "All good" +msgstr "全部正常" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "B" +msgstr "B" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:884 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:885 +msgid "Busiest radio" +msgstr "最繁忙射频" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:235 +msgid "Busiest radio: %s of Wi-Fi traffic" +msgstr "最繁忙射频:占 Wi-Fi 流量的 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:982 +msgid "Busy share of each CPU core" +msgstr "每个 CPU 核心的繁忙占比" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:69 +msgid "Busy · load %s" +msgstr "繁忙 · 负载 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:70 +msgid "Busy; load %s on %d cores; see the busiest processes" +msgstr "繁忙;负载 %s(%d 核);查看最繁忙进程" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:67 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:68 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:976 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1944 +msgid "CPU" +msgstr "CPU" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:80 +msgid "Cache (reclaimable)" +msgstr "缓存(可回收)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1812 +msgid "Cancel" +msgstr "取消" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1719 +msgid "Capabilities" +msgstr "能力" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1906 +msgid "Carrier" +msgstr "链路状态" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1874 +msgid "Centre frequency" +msgstr "中心频率" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1281 +msgid "Ch %d" +msgstr "信道 %d" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:827 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1249 +msgid "Ch %d · %d MHz" +msgstr "信道 %d · %d MHz" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1867 +msgid "Channel" +msgstr "信道" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:827 +msgid "Channel —" +msgstr "信道 —" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1313 +msgid "Clear filter" +msgstr "清除筛选" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:775 +msgid "Click any node for details" +msgstr "点击任意节点查看详情" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:882 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1286 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1321 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1322 +msgid "Clients" +msgstr "客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1879 +msgid "Clients on this radio" +msgstr "此射频上的客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1582 +msgid "Close" +msgstr "关闭" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1501 +msgid "Connected" +msgstr "已连接" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1463 +msgid "Connected after you opened this page" +msgstr "打开本页面后连接" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1725 +msgid "Connected for" +msgstr "已连接时长" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1722 +msgid "Connection" +msgstr "连接" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1004 +msgid "Core %d: %s" +msgstr "核心 %d:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1795 +msgid "Could not save the name (no permission or the device refused it)." +msgstr "无法保存名称(无权限或设备拒绝)。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1917 +msgid "Counters since boot" +msgstr "自启动以来的计数器" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1877 +msgid "Country" +msgstr "国家" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "DHCP" +msgstr "DHCP" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1750 +msgid "DHCP hostname" +msgstr "DHCP 主机名" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1915 +msgid "DNS" +msgstr "DNS" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:795 +msgid "DNS %s" +msgstr "DNS %s" + +#: luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json:3 +msgid "Dashboard" +msgstr "仪表盘" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:268 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1468 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1935 +msgid "Device" +msgstr "设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1779 +msgid "Device name" +msgstr "设备名" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1808 +msgid "Device type" +msgstr "设备类型" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1059 +msgid "Direction" +msgstr "方向" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:828 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1251 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +msgid "Disabled" +msgstr "已禁用" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1688 +msgid "Disconnected %s; shown from the last sample." +msgstr "断开于 %s;显示来自最后一次采样。" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:44 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:828 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1251 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +msgid "Down" +msgstr "断开" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1061 +msgid "Download" +msgstr "下载" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:256 +msgid "Download (to clients)" +msgstr "下载(到客户端)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1730 +msgid "Downloaded" +msgstr "已下载" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1920 +msgid "Dropped" +msgstr "已丢弃" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 +msgid "Edit" +msgstr "编辑" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:25 +msgid "Enhanced Open (OWE)" +msgstr "增强开放 (OWE)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:121 +msgid "Everything looks healthy" +msgstr "一切看起来正常" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:117 +msgid "Excellent" +msgstr "极好" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1495 +msgid "Experience" +msgstr "体验" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:119 +msgid "Fair" +msgstr "一般" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:136 +msgid "Fair signal" +msgstr "信号一般" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:136 +msgid "Fair signal %s; lower rates at range" +msgstr "信号一般 %s;远距离速率较低" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1310 +msgid "Filter by name, address, SSID…" +msgstr "按名称、地址、SSID…筛选" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1311 +msgid "Filter clients" +msgstr "筛选客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1938 +msgid "Firmware" +msgstr "固件" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1732 +msgid "Frames to device" +msgstr "发往设备的帧" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:81 +msgid "Free" +msgstr "空闲" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1717 +msgid "From device" +msgstr "来自设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1484 +msgid "From device: %s" +msgstr "来自设备:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1901 +msgid "From network" +msgstr "来自网络" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:793 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1914 +msgid "Gateway" +msgstr "网关" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "Gbit/s" +msgstr "Gbit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1718 +msgid "Generation" +msgstr "代际" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "GiB" +msgstr "GiB" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Globally unique" +msgstr "全局唯一" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Globally unique · %s" +msgstr "全局唯一 · %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:118 +msgid "Good" +msgstr "良好" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:137 +msgid "Good signal" +msgstr "信号良好" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:137 +msgid "Good signal %s" +msgstr "信号良好 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 +msgid "Half duplex" +msgstr "半双工" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:633 +msgid "Health checks" +msgstr "健康检查" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1565 +msgid "Hidden" +msgstr "隐藏" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 +msgid "IP addresses" +msgstr "IP 地址" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1912 +msgid "IPv4" +msgstr "IPv4" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1913 +msgid "IPv6" +msgstr "IPv6" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1734 +msgid "Identity" +msgstr "身份" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:79 +msgid "In use" +msgstr "使用中" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1909 +msgid "Interface up for" +msgstr "接口已启用时长" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1899 +msgid "Interfaces" +msgstr "接口" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:793 +msgid "Internet" +msgstr "互联网" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1939 +msgid "Kernel" +msgstr "内核" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1044 +msgid "Kernel %s" +msgstr "内核 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "KiB" +msgstr "KiB" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1726 +msgid "Last activity" +msgstr "最后活动" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1462 +msgid "Left %s" +msgstr "离开于 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1464 +msgid "Legacy" +msgstr "旧版" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1715 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1905 +msgid "Link" +msgstr "链路" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1483 +msgid "Link rate" +msgstr "链路速率" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:739 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1488 +msgid "Live" +msgstr "实时" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1943 +msgid "Load" +msgstr "负载" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:70 +msgid "Load %s on %d cores" +msgstr "负载 %s(%d 核)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:69 +msgid "Load %s · %d cores" +msgstr "负载 %s · %d 核" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1945 +msgid "Load average" +msgstr "平均负载" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:965 +msgid "Load average over 1, 5 and 15 minutes" +msgstr "1、5、15 分钟平均负载" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:999 +msgid "Load avg %s" +msgstr "平均负载 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1941 +msgid "Local time" +msgstr "本地时间" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:142 +msgid "Low link rate %s despite good signal; interference or an old device" +msgstr "信号良好但链路速率低 %s;可能存在干扰或旧设备" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:142 +msgid "Low link rate for its signal" +msgstr "相对其信号链路速率偏低" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 +msgid "MAC" +msgstr "MAC" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1735 +msgid "MAC address" +msgstr "MAC 地址" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:106 +msgid "MCS %d" +msgstr "MCS %d" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1752 +msgid "Manufacturer of the network adapter" +msgstr "网卡制造商" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "Mbit/s" +msgstr "Mbit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:67 +msgid "Measuring over the next poll" +msgstr "下次轮询中测量" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:67 +msgid "Measuring…" +msgstr "测量中…" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:77 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:986 +msgid "Memory" +msgstr "内存" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1946 +msgid "Memory in use" +msgstr "已用内存" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "MiB" +msgstr "MiB" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1873 +msgid "Mode" +msgstr "模式" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1936 +msgid "Model" +msgstr "型号" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1807 +msgid "Name" +msgstr "名称" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:134 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1845 +msgid "Name contains invisible or control characters" +msgstr "名称包含不可见或控制字符" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1738 +msgid "Name from" +msgstr "名称来源" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1473 +msgid "Name from: %s" +msgstr "名称来源:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:132 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:137 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1846 +msgid "Name is longer than %d characters" +msgstr "名称超过 %d 个字符" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:131 +msgid "Name must be text" +msgstr "名称必须是文本" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1679 +msgid "Name this device" +msgstr "命名此设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1480 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1723 +msgid "Network" +msgstr "网络" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:774 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:775 +msgid "Network path" +msgstr "网络路径" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1379 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1463 +msgid "New" +msgstr "新增" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:849 +msgid "No SSIDs" +msgstr "无 SSID" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +msgid "No clients" +msgstr "无客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1447 +msgid "No clients connected" +msgstr "无客户端连接" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1518 +msgid "No clients connected." +msgstr "没有客户端连接。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1450 +msgid "No clients in this view" +msgstr "此视图中没有客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1450 +msgid "No clients match “%s”" +msgstr "没有匹配“%s”的客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1884 +msgid "No clients; an idle radio is fine." +msgstr "无客户端;空闲的射频是正常的。" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:238 +msgid "No clients; idle radios are fine" +msgstr "无客户端;空闲的射频是正常的" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:794 +msgid "No default route" +msgstr "无默认路由" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:44 +msgid "No interface with a route" +msgstr "无带路由的接口" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1891 +msgid "No interface with a route is up." +msgstr "没有带路由的接口处于启用状态。" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1906 +msgid "No link" +msgstr "无链路" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:45 +msgid "No network interface with a route is up" +msgstr "没有带路由的网络接口处于启用状态" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:161 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 +msgid "No problems detected" +msgstr "未检测到问题" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:853 +msgid "No radios reported" +msgstr "未报告射频" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:116 +msgid "No signal" +msgstr "无信号" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +msgid "No stations are associated right now" +msgstr "当前没有站点关联" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1554 +msgid "No wireless networks configured." +msgstr "未配置无线网络。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1256 +msgid "No wireless radios are reported by this device." +msgstr "此设备未报告任何无线射频。" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +msgid "Nobody associated" +msgstr "无关联站点" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1518 +msgid "Nobody is moving data right now." +msgstr "当前没有设备在传输数据。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1288 +msgid "Noise" +msgstr "噪声" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1720 +msgid "Noise / SNR" +msgstr "噪声 / 信噪比" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1876 +msgid "Noise floor" +msgstr "噪声底" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:237 +msgid "Noise floor not reported by the driver" +msgstr "驱动未报告噪声底" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +msgid "None" +msgstr "无" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:150 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1844 +msgid "Not a station MAC address" +msgstr "不是站点 MAC 地址" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 +msgid "Not connected" +msgstr "未连接" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1024 +msgid "Not reported" +msgstr "未报告" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1755 +msgid "Nothing better known" +msgstr "无更多信息" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1753 +msgid "Nothing better known; the address is randomised" +msgstr "无更多信息;地址已随机化" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:25 +msgid "OWE" +msgstr "OWE" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:153 +msgid "Older %s device" +msgstr "较旧的 %s 设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1464 +msgid "Older Wi-Fi generation than the radio; uses more airtime" +msgstr "比射频支持的 Wi-Fi 代际更旧;占用更多空口时间" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +msgid "Online" +msgstr "在线" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:24 +msgid "Open" +msgstr "开放" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1430 +msgid "Open details of %s" +msgstr "打开 %s 的详情" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:24 +msgid "Open network" +msgstr "开放网络" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:451 +msgid "OpenWrt device" +msgstr "OpenWrt 设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1719 +msgid "PMF" +msgstr "PMF" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:737 +msgid "Paused while hidden" +msgstr "隐藏时暂停" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "PiB" +msgstr "PiB" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1937 +msgid "Platform" +msgstr "平台" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:157 +msgid "Poor" +msgstr "较差" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 +msgid "Private" +msgstr "私有" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1736 +msgid "Private Wi-Fi address (randomised by the device)" +msgstr "私有 Wi-Fi 地址(设备随机化)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:266 +msgid "Private device" +msgstr "私有设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1934 +msgid "Processes" +msgstr "进程" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1908 +msgid "Protocol" +msgstr "协议" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1315 +msgid "Quick filters" +msgstr "快速筛选" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1724 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1871 +msgid "Radio" +msgstr "射频" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1277 +msgid "Radio details" +msgstr "射频详情" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:233 +msgid "Radio is disabled" +msgstr "射频已禁用" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:234 +msgid "Radio is enabled but not up" +msgstr "射频已启用但未上线" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1300 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1865 +msgid "Radio settings" +msgstr "射频设置" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1229 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1231 +msgid "Radios" +msgstr "射频" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:795 +msgid "Reachability not probed" +msgstr "未探测可达性" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1899 +msgid "Realtime graphs" +msgstr "实时图表" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1918 +msgid "Received" +msgstr "已接收" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:738 +msgid "Reconnecting…" +msgstr "重新连接中…" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1813 +msgid "Remove name" +msgstr "移除名称" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1679 +msgid "Rename" +msgstr "重命名" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1748 +msgid "Reverse DNS of its address" +msgstr "其地址的反向 DNS" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:463 +msgid "Root (overlay)" +msgstr "Root(overlay)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1805 +msgid "Save" +msgstr "保存" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1727 +msgid "Seen since" +msgstr "首次出现" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1919 +msgid "Sent" +msgstr "已发送" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1451 +msgid "Show all clients" +msgstr "显示全部客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:973 +msgid "Show or hide system details" +msgstr "显示/隐藏系统详情" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:775 +msgid "Show or hide the network path" +msgstr "显示/隐藏网络路径" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1232 +msgid "Show or hide the radio details" +msgstr "显示/隐藏射频详情" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1074 +msgid "Show or hide the throughput chart" +msgstr "显示/隐藏吞吐量图表" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 +msgid "Show or hide the wireless networks" +msgstr "显示/隐藏无线网络" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:839 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1561 +msgid "Show the clients of %s" +msgstr "显示 %s 的客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:99 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:100 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:103 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1390 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1477 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1703 +msgid "Signal" +msgstr "信号强度" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:133 +msgid "Signal not reported" +msgstr "未报告信号" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1907 +msgid "Speed" +msgstr "速率" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1448 +msgid "Stations appear here as soon as they associate with one of the SSIDs." +msgstr "站点一旦关联到任一 SSID 便会显示在这里。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +msgid "Status" +msgstr "状态" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:108 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:989 +msgid "Storage" +msgstr "存储" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1814 +msgid "Stored on this device in /etc/config/vantage" +msgstr "保存在本设备 /etc/config/vantage 中" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 +msgid "Strong signal" +msgstr "信号强" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:161 +msgid "Strong signal %s, %s link" +msgstr "信号强 %s,链路 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:163 +msgid "Strong signal, %s" +msgstr "信号强,%s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1029 +msgid "Swap" +msgstr "交换分区" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:972 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:973 +msgid "System" +msgstr "系统" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1934 +msgid "System settings" +msgstr "系统设置" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "Tbit/s" +msgstr "Tbit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:463 +msgid "Temporary (RAM)" +msgstr "临时(RAM)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1849 +msgid "The device could not write /etc/config/vantage." +msgstr "设备无法写入 /etc/config/vantage。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1288 +msgid "" +"The driver reports a placeholder value, so no noise floor or SNR is shown" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1751 +msgid "The name the device announces (WPS)" +msgstr "设备广播的名称(WPS)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:156 +msgid "The stored name has an invalid section id" +msgstr "保存的名称包含无效的节 ID" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:812 +msgid "This access point" +msgstr "此接入点" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1848 +msgid "This device already stores %d names; remove some first." +msgstr "此设备已存储 %d 个名称;请先移除部分。" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +msgid "This device reports no radios" +msgstr "此设备未报告任何射频" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1856 +msgid "This radio is not reported any more." +msgstr "此射频不再被报告。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1661 +msgid "This station is not associated right now." +msgstr "此站点当前未关联。" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1071 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1073 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1709 +msgid "Throughput" +msgstr "吞吐量" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1064 +msgid "Throughput per link; arrow keys read the values over time" +msgstr "每条链路的吞吐量;方向键可查看历史值" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:53 +msgid "TiB" +msgstr "TiB" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1716 +msgid "To device" +msgstr "到设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1492 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1533 +msgid "To device %s, from device %s" +msgstr "到设备 %s,来自设备 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1484 +msgid "To device: %s" +msgstr "到设备:%s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1902 +msgid "To network" +msgstr "发往网络" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1509 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1510 +msgid "Top talkers" +msgstr "流量大户" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1729 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1868 +msgid "Traffic" +msgstr "流量" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:883 +msgid "Traffic now" +msgstr "当前流量" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1287 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1875 +msgid "Tx power" +msgstr "发射功率" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1660 +msgid "Unknown device" +msgstr "未知设备" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1847 +msgid "Unknown device type" +msgstr "未知设备类型" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1856 +msgid "Unknown radio" +msgstr "未知射频" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:56 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1276 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1872 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1906 +msgid "Up" +msgstr "正常" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:996 +msgid "Up %s" +msgstr "已运行 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:44 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:46 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:48 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:50 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:56 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1086 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1891 +msgid "Uplink" +msgstr "上行链路" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1086 +msgid "Uplink %s" +msgstr "上行 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1895 +msgid "Uplink · %s" +msgstr "上行 · %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1061 +msgid "Upload" +msgstr "上传" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:257 +msgid "Upload (from clients)" +msgstr "上传(来自客户端)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1731 +msgid "Uploaded" +msgstr "已上传" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1940 +msgid "Uptime" +msgstr "运行时间" + +#: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:24 +msgid "" +"Vantage dashboard: client names from reverse DNS (lets the holder make the " +"device send DNS PTR queries to any DNS server and port it names)" +msgstr "" + +#: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:32 +msgid "" +"Vantage dashboard: name stations (luci.vantage set_alias: validated names " +"in /etc/config/vantage only, at most 512)" +msgstr "" + +#: luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json:3 +msgid "" +"Vantage dashboard: read-only status of the device, uplink, radios and " +"stations (wireless configuration without keys, through luci.vantage wireless)" +msgstr "" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:43 +msgid "Vendor" +msgstr "供应商" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:134 +msgid "Very weak signal %s; expect drops" +msgstr "信号极弱 %s;可能出现掉线" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:134 +msgid "Very weak signal; expect drops" +msgstr "信号极弱;可能出现掉线" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:36 +msgid "WEP" +msgstr "WEP" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:36 +msgid "WEP (insecure)" +msgstr "WEP(不安全)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:30 +msgid "WPA" +msgstr "WPA" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:35 +msgid "WPA-Ent" +msgstr "WPA 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:35 +msgid "WPA-Enterprise (legacy)" +msgstr "WPA 企业版(旧版)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:30 +msgid "WPA-Personal (legacy)" +msgstr "WPA 个人版(旧版)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:28 +msgid "WPA/WPA2" +msgstr "WPA/WPA2" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:33 +msgid "WPA/WPA2-Ent" +msgstr "WPA/WPA2 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:33 +msgid "WPA/WPA2-Enterprise" +msgstr "WPA/WPA2 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:28 +msgid "WPA/WPA2-Personal (TKIP allowed)" +msgstr "WPA/WPA2 个人版(允许 TKIP)" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:29 +msgid "WPA2" +msgstr "WPA2" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:34 +msgid "WPA2-Ent" +msgstr "WPA2 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:34 +msgid "WPA2-Enterprise" +msgstr "WPA2 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:29 +msgid "WPA2-Personal" +msgstr "WPA2 个人版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:26 +msgid "WPA2/3" +msgstr "WPA2/3" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:31 +msgid "WPA2/3-Ent" +msgstr "WPA2/3 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:31 +msgid "WPA2/WPA3-Enterprise" +msgstr "WPA2/WPA3 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:26 +msgid "WPA2/WPA3-Personal" +msgstr "WPA2/WPA3 个人版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:27 +msgid "WPA3" +msgstr "WPA3" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:32 +msgid "WPA3-Ent" +msgstr "WPA3 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:32 +msgid "WPA3-Enterprise" +msgstr "WPA3 企业版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:27 +msgid "WPA3-Personal" +msgstr "WPA3 个人版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "WPS" +msgstr "WPS" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1739 +msgid "WPS device name" +msgstr "WPS 设备名称" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:120 +msgid "Weak" +msgstr "弱" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:135 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1378 +msgid "Weak signal" +msgstr "信号弱" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:135 +msgid "Weak signal %s; may roam poorly" +msgstr "信号弱 %s;漫游可能不佳" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:886 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:887 +msgid "Weakest signal" +msgstr "最弱信号" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:86 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:87 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:89 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:91 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1544 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1545 +msgid "Wireless networks" +msgstr "无线网络" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1689 +msgid "Wireless settings" +msgstr "无线设置" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1747 +msgid "Your name for this device" +msgstr "你为此设备设置的名称" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1282 +msgid "auto" +msgstr "自动" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1131 +msgid "avg" +msgstr "平均" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1727 +msgid "before this page opened" +msgstr "打开本页之前" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "bit/s" +msgstr "bit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1156 +msgid "collecting · %s of data" +msgstr "采集中 · 已采集 %s 数据" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1156 +msgid "collecting · first samples" +msgstr "采集中 · 首个样本" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:197 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1137 +msgid "collecting…" +msgstr "采集中…" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:97 +msgid "dBm" +msgstr "dBm" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1779 +msgid "e.g. Living room TV" +msgstr "例如:客厅电视" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 +msgid "from clients" +msgstr "来自客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:169 +msgid "full duplex" +msgstr "全双工" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:59 +msgid "gateway %s" +msgstr "网关 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/model.js:169 +msgid "half duplex" +msgstr "半双工" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1570 +msgid "idle" +msgstr "空闲" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:88 +msgid "just now" +msgstr "刚刚" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/fmt.js:30 +msgid "kbit/s" +msgstr "kbit/s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:981 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 +msgid "last 5 min" +msgstr "最近 5 分钟" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:153 +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:104 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1486 +msgid "legacy" +msgstr "旧版" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/names.js:42 +msgid "mDNS" +msgstr "mDNS" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1749 +msgid "mDNS announcement" +msgstr "mDNS 广播" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:883 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1711 +msgid "measuring" +msgstr "测量中" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1132 +msgid "measuring…" +msgstr "测量中…" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:885 +msgid "no Wi-Fi traffic yet" +msgstr "尚无 Wi-Fi 流量" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:887 +msgid "no clients" +msgstr "无客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:804 +msgid "no uplink" +msgstr "无上行链路" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:882 +msgid "none connected" +msgstr "无连接" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1737 +msgid "not known to this AP" +msgstr "此 AP 未知" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1288 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1289 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1876 +msgid "not reported" +msgstr "未报告" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1876 +msgid "not reported (driver placeholder %s)" +msgstr "未报告(驱动占位 %s)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1720 +msgid "not reported by the driver" +msgstr "驱动未报告" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1152 +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1726 +msgid "now" +msgstr "现在" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/insight.js:60 +msgid "now %s down, %s up" +msgstr "当前下行 %s,上行 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1718 +msgid "older than the radio (%s)" +msgstr "比射频支持的标准更旧(%s)" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1130 +msgid "peak" +msgstr "峰值" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:982 +msgid "per core" +msgstr "每核" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1705 +msgid "range %s … %s" +msgstr "范围 %s … %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1901 +msgid "received on %s" +msgstr "在 %s 上接收" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1902 +msgid "sent on %s" +msgstr "在 %s 上发送" + +#: luci-app-vantage/htdocs/luci-static/resources/vantage/wifi.js:108 +msgid "short GI" +msgstr "短 GI" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1155 +msgid "to clients" +msgstr "发往客户端" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:815 +msgid "up %s" +msgstr "已运行 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1510 +msgid "who uses the bandwidth" +msgstr "带宽使用者" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:883 +msgid "↑ %s · via %s" +msgstr "↑ %s · 经 %s" + +#: luci-app-vantage/htdocs/luci-static/resources/view/vantage/overview.js:1149 +msgid "−%d min" +msgstr "−%d 分钟" + +#, javascript-format +#~ msgid "%d MHz" +#~ msgstr "%d MHz" + +#~ msgid "Vantage dashboard" +#~ msgstr "Vantage 仪表盘" + +#~ msgid "" +#~ "Live network dashboard (landing page under admin/dashboard): device, " +#~ "uplink, radios, SSIDs and stations with health and experience scores. " +#~ "Read-only except for station names, which are kept in /etc/config/vantage." +#~ msgstr "" +#~ "实时网络仪表盘(位于 admin/dashboard 的首页):设备、上行链路、射频、SSID " +#~ "和站点,并带有健康与体验评分。除站点名称外均为只读,站点名称保存在 /etc/" +#~ "config/vantage 中。" diff --git a/luci-app-vantage/root/etc/config/vantage b/luci-app-vantage/root/etc/config/vantage new file mode 100644 index 000000000..872d2b68e --- /dev/null +++ b/luci-app-vantage/root/etc/config/vantage @@ -0,0 +1,11 @@ +# Vantage dashboard settings (luci-app-vantage). +# +# Station names given in the dashboard's inspector are stored here, one +# section per station (MAC in lower case, name at most 48 characters): +# +# config client +# option mac '00:00:5e:00:53:01' +# option name 'Living room TV' +# option icon 'tv' + +config vantage 'main' diff --git a/luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json b/luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json new file mode 100644 index 000000000..ad0a16688 --- /dev/null +++ b/luci-app-vantage/root/usr/share/luci/menu.d/luci-app-vantage.json @@ -0,0 +1,13 @@ +{ + "admin/dashboard": { + "title": "Dashboard", + "order": 1, + "action": { + "type": "view", + "path": "vantage/overview" + }, + "depends": { + "acl": [ "luci-app-vantage" ] + } + } +} diff --git a/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json b/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json new file mode 100644 index 000000000..439bd02d6 --- /dev/null +++ b/luci-app-vantage/root/usr/share/rpcd/acl.d/luci-app-vantage.json @@ -0,0 +1,45 @@ +{ + "luci-app-vantage": { + "description": "Vantage dashboard: read-only status of the device, uplink, radios and stations (wireless configuration without keys, through luci.vantage wireless)", + "read": { + "file": { + "/proc/stat": [ "read" ] + }, + "ubus": { + "file": [ "read" ], + "system": [ "board", "info" ], + "network.interface": [ "dump" ], + "network.device": [ "status" ], + "luci-rpc": [ "getHostHints" ], + "luci.vantage": [ "wireless" ], + "iwinfo": [ "assoclist", "info" ], + "hostapd.*": [ "get_clients", "get_status" ], + "umdns": [ "hosts" ], + "uci": [ "get" ] + }, + "uci": [ "vantage" ] + } + }, + "luci-app-vantage-rdns": { + "description": "Vantage dashboard: client names from reverse DNS (lets the holder make the device send DNS PTR queries to any DNS server and port it names)", + "read": { + "ubus": { + "network.rrdns": [ "lookup" ] + } + } + }, + "luci-app-vantage-names": { + "description": "Vantage dashboard: name stations (luci.vantage set_alias: validated names in /etc/config/vantage only, at most 512)", + "read": { + "ubus": { + "uci": [ "get" ] + }, + "uci": [ "vantage" ] + }, + "write": { + "ubus": { + "luci.vantage": [ "set_alias" ] + } + } + } +} diff --git a/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage b/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage new file mode 100644 index 000000000..f7efbd0f3 --- /dev/null +++ b/luci-app-vantage/root/usr/share/rpcd/ucode/luci.vantage @@ -0,0 +1,332 @@ +// luci-app-vantage: server side of the Vantage dashboard. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// ubus object luci.vantage, loaded by rpcd-mod-ucode: +// +// wireless network.wireless status reduced to the fields the dashboard +// reads: no key, sae_password, RADIUS or 802.11r secrets, no +// EAP credentials. Read group luci-app-vantage. +// set_alias { mac, name, icon }: name one station, or remove its name +// with name ''. Validated here, whatever the browser did, and +// written to /etc/config/vantage only. Write group +// luci-app-vantage-names. +// +// Keep the validation in step with names.js (validateAlias, normMac, +// ICONS) and with dev/replay/vantage-plugin.js; tests/plugin.test.js runs +// all three against the same cases. + +'use strict'; + +import { cursor } from 'uci'; +import { connect } from 'ubus'; + +const CONFIG = 'vantage'; +const NAME_MAX = 48; // code points, after collapsing spaces +const NAME_MAX_UNITS = NAME_MAX * 8; // raw input cap, in UTF-16 code units (as names.js) +const MAX_ALIASES = 512; // client sections; bounds /etc/config/vantage +const ICONS = [ 'phone', 'laptop', 'tablet', 'desktop', 'tv', 'speaker', 'printer', 'camera', 'console', 'watch', 'iot', 'router', 'device' ]; + +/* ------------------------------------------------------------ helpers */ + +// '02-00-5E-00-53-0F' / '0200.5e00.530f' / '02005e00530f' -> '02:00:5e:00:53:0f' +function norm_mac(v) { + if (type(v) != 'string') + return null; + + let h = lc(trim(v)); + + if (match(h, /^[0-9a-f]{2}(:[0-9a-f]{2}){5}$/)) + return h; + + if (match(h, /^[0-9a-f]{2}(-[0-9a-f]{2}){5}$/)) + return replace(h, /-/g, ':'); + + if (match(h, /^[0-9a-f]{4}\.[0-9a-f]{4}\.[0-9a-f]{4}$/) || match(h, /^[0-9a-f]{12}$/)) { + h = replace(h, /\./g, ''); + return join(':', map(match(h, /../g), (m) => m[0])); + } + + return null; +} + +function is_multicast(mac) { + return (hex(substr(mac, 0, 2)) & 1) == 1; +} + +// UTF-8 -> [ [ code point, byte offset, byte length ], ... ]; null when the +// bytes are not well-formed UTF-8 (overlong forms and surrogates included) +function decode(s) { + let out = [], n = length(s), i = 0; + + while (i < n) { + let b = ord(s, i), cp, need; + + if (b < 0x80) { cp = b; need = 0; } + else if (b >= 0xc2 && b <= 0xdf) { cp = b & 0x1f; need = 1; } + else if (b >= 0xe0 && b <= 0xef) { cp = b & 0x0f; need = 2; } + else if (b >= 0xf0 && b <= 0xf4) { cp = b & 0x07; need = 3; } + else return null; + + if (i + need >= n) + return null; + + for (let k = 1; k <= need; k++) { + let c = ord(s, i + k); + + if ((c & 0xc0) != 0x80) + return null; + + cp = (cp << 6) | (c & 0x3f); + } + + if ((need == 2 && cp < 0x800) || (need == 3 && (cp < 0x10000 || cp > 0x10ffff)) || (cp >= 0xd800 && cp <= 0xdfff)) + return null; + + push(out, [ cp, i, need + 1 ]); + i += need + 1; + } + + return out; +} + +// control, invisible, bidi and format characters: the C0/C1 controls plus +// fmt.js UNSAFE_TEXT +function forbidden(cp) { + return cp <= 0x1f || (cp >= 0x7f && cp <= 0x9f) || + cp == 0xad || cp == 0x34f || cp == 0x61c || cp == 0x115f || cp == 0x1160 || + cp == 0x17b4 || cp == 0x17b5 || cp == 0x180e || cp == 0x200b || cp == 0x200e || cp == 0x200f || + (cp >= 0x2028 && cp <= 0x202e) || (cp >= 0x2060 && cp <= 0x206f) || cp == 0x3164 || + (cp >= 0xfe00 && cp <= 0xfe0d) || cp == 0xfeff || cp == 0xffa0 || (cp >= 0xfff9 && cp <= 0xfffb) || + (cp >= 0xe0000 && cp <= 0xe007f); +} + +// space characters that collapse to one U+0020 +function is_space(cp) { + return cp == 0x20 || cp == 0xa0 || cp == 0x1680 || (cp >= 0x2000 && cp <= 0x200a) || + cp == 0x202f || cp == 0x205f || cp == 0x3000; +} + +// names.js validateAlias: -> { ok: true, value } ('' clears) or { ok: false, error } +function validate_name(name) { + if (name == null) + name = ''; + + if (type(name) != 'string') + return { ok: false, error: 'invalid-name' }; + + let cps = decode(name); + + if (cps == null) + return { ok: false, error: 'invalid-name' }; + + let units = 0; + + for (let c in cps) + units += (c[0] > 0xffff) ? 2 : 1; + + if (units > NAME_MAX_UNITS) + return { ok: false, error: 'name-too-long' }; + + for (let c in cps) + if (forbidden(c[0])) + return { ok: false, error: 'invalid-name' }; + + // collapse runs of spaces, trim both ends + let out = '', count = 0, pending = false; + + for (let c in cps) { + if (is_space(c[0])) { + pending = true; + continue; + } + + if (pending && count) { + out += ' '; + count++; + } + + pending = false; + out += substr(name, c[1], c[2]); + count++; + } + + if (count > NAME_MAX) + return { ok: false, error: 'name-too-long' }; + + return { ok: true, value: out }; +} + +function scalar(v) { + return (type(v) in [ 'string', 'int', 'double', 'bool' ]) ? v : null; +} + +function short_string(v, max) { + return (type(v) == 'string' && length(v) <= max) ? v : null; +} + +/* ----------------------------------------------------------- wireless */ + +// the encryption mode ('sae-mixed+ccmp', 'psk2', 'none', ...): its leading +// run of mode characters and nothing after it +function encryption(v) { + let m = (type(v) == 'string') ? match(v, /^[A-Za-z0-9+_-]{1,40}/) : null; + return m ? m[0] : null; +} + +function reduce_iface(i) { + let c = (type(i.config) == 'object') ? i.config : {}; + let networks = []; + + for (let n in ((type(c.network) == 'array') ? c.network : [ c.network ])) + if (type(n) == 'string' && length(n) <= 64) + push(networks, n); + + return { + ifname: short_string(i.ifname, 64), + section: short_string(i.section, 64), + config: { + ssid: short_string(c.ssid, 128), + mode: short_string(c.mode, 32), + hidden: scalar(c.hidden), + encryption: encryption(c.encryption), + ieee80211w: scalar(c.ieee80211w), + ieee80211r: scalar(c.ieee80211r), + ieee80211k: scalar(c.ieee80211k), + ieee80211v: scalar(c.ieee80211v), + network: networks, + macaddr: norm_mac(c.macaddr) + } + }; +} + +function reduce_radio(r) { + let c = (type(r.config) == 'object') ? r.config : {}; + let ifaces = []; + + for (let i in ((type(r.interfaces) == 'array') ? r.interfaces : [])) + if (type(i) == 'object') + push(ifaces, reduce_iface(i)); + + return { + up: r.up === true, + pending: r.pending === true, + disabled: r.disabled === true, + config: { + band: short_string(c.band, 16), + channel: scalar(c.channel), + htmode: short_string(c.htmode, 16), + country: short_string(c.country, 8), + txpower: scalar(c.txpower), + disabled: scalar(c.disabled) + }, + interfaces: ifaces + }; +} + +/* ---------------------------------------------------------- set_alias */ + +function set_alias(args) { + let mac = norm_mac(args.mac); + + if (!mac || is_multicast(mac)) + return { ok: false, error: 'invalid-mac' }; + + let v = validate_name(args.name); + + if (!v.ok) + return v; + + let icon = args.icon ?? ''; + + if (icon != '' && !(icon in ICONS)) + return { ok: false, error: 'invalid-icon' }; + + let uci = cursor(); + let mine = [], total = 0; + + uci.load(CONFIG); + uci.foreach(CONFIG, 'client', (s) => { + total++; + + if (norm_mac(s.mac) == mac) + push(mine, s['.name']); + }); + + let op; + + if (v.value == '') { + if (!length(mine)) + return { ok: true, op: 'none', mac }; + + for (let sid in mine) + uci.delete(CONFIG, sid); + + op = 'delete'; + } + else if (length(mine)) { + // one section per MAC: update the first, drop duplicates + uci.set(CONFIG, mine[0], 'mac', mac); + uci.set(CONFIG, mine[0], 'name', v.value); + + if (icon != '') + uci.set(CONFIG, mine[0], 'icon', icon); + else + uci.delete(CONFIG, mine[0], 'icon'); + + for (let sid in slice(mine, 1)) + uci.delete(CONFIG, sid); + + op = 'set'; + } + else { + if (total >= MAX_ALIASES) + return { ok: false, error: 'too-many' }; + + let sid = uci.add(CONFIG, 'client'); + + if (!sid) + return { ok: false, error: 'failed' }; + + uci.set(CONFIG, sid, 'mac', mac); + uci.set(CONFIG, sid, 'name', v.value); + + if (icon != '') + uci.set(CONFIG, sid, 'icon', icon); + + op = 'add'; + } + + if (!uci.commit(CONFIG)) + return { ok: false, error: 'failed' }; + + return { ok: true, op, mac }; +} + +/* ------------------------------------------------------------ methods */ + +const methods = { + wireless: { + call: function() { + let reply = connect()?.call('network.wireless', 'status'); + let out = {}; + + if (type(reply) != 'object') + return out; + + for (let name, r in reply) + if (type(r) == 'object' && match(name, /^[A-Za-z0-9_.-]{1,64}$/)) + out[name] = reduce_radio(r); + + return out; + } + }, + + set_alias: { + args: { mac: 'mac', name: 'name', icon: 'icon' }, + call: function(request) { + return set_alias(request.args); + } + } +}; + +return { 'luci.vantage': methods }; diff --git a/luci-mod-fluentdashboard/Makefile b/luci-mod-fluentdashboard/Makefile new file mode 100644 index 000000000..d0efff9e2 --- /dev/null +++ b/luci-mod-fluentdashboard/Makefile @@ -0,0 +1,17 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Fluent dashboard for LuCI +LUCI_DEPENDS:=+luci-base +libiwinfo +LUCI_DESCRIPTION:=A fixed-card, theme-independent LuCI status dashboard with system, network, wireless, DHCP lease, and wireless client information. +LUCI_MAINTAINER:=LazuliKao +LUCI_MINIFY_CSS:=0 +LUCI_MINIFY_JS:=0 + +PKG_NAME:=luci-mod-fluentdashboard +PKG_VERSION:=1.0.9 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 +PKG_BUILD_DEPENDS:=iwinfo + +# call BuildPackage - OpenWrt buildroot signature +include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-mod-fluentdashboard/htdocs/luci-static/resources/view/fluentdashboard/index.css b/luci-mod-fluentdashboard/htdocs/luci-static/resources/view/fluentdashboard/index.css new file mode 100644 index 000000000..9195e80ee --- /dev/null +++ b/luci-mod-fluentdashboard/htdocs/luci-static/resources/view/fluentdashboard/index.css @@ -0,0 +1,241 @@ +.fluent-dashboard { + --fluent-dashboard-border: var(--fluent-card-border, var(--inputborder-color, color-mix(in srgb, currentColor 16%, transparent))); + --fluent-dashboard-muted: var(--fluent-text-secondary, color-mix(in srgb, currentColor 68%, transparent)); + --fluent-dashboard-surface: var(--fluent-card-bg, rgba(var(--primary-rgbbody, 255, 255, 255), 0.7)); + --fluent-dashboard-shadow: var(--fluent-card-shadow, 0 .1rem .3rem var(--input-boxcolor, color-mix(in srgb, CanvasText 8%, transparent))); + color: var(--fluent-text, CanvasText); + container-type: inline-size; +} + +.fluent-dashboard, .fluent-dashboard * { + box-sizing: border-box; +} + +.fluent-dashboard__header { + color: highlighttext; + background: linear-gradient(120deg, Highlight, color-mix(in srgb, Highlight 68%, CanvasText)); + border-radius: .75rem; + justify-content: space-between; + align-items: end; + gap: 1rem; + margin-block-end: 1rem; + padding: 1.25rem; + display: flex; + z-index: 0; + overflow: hidden; + position: relative; +} + +.fluent-dashboard__header h1, .fluent-dashboard__header p { + color: inherit; + margin: 0; +} + +.fluent-dashboard__header h1 { + font-size: clamp(1.5rem, 4vw, 2.25rem); + line-height: 1.1; +} + +.fluent-dashboard__header h1 + p { + opacity: .82; + margin-block-start: .4rem; +} + +.fluent-dashboard__updated { + opacity: .86; + flex: none; + font-size: .85rem; +} + +.fluent-dashboard__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1rem; + display: grid; +} + +.fluent-dashboard__card { + min-width: 0; + color: inherit; + background: var(--fluent-dashboard-surface); + border: 1px solid var(--fluent-dashboard-border); + box-shadow: var(--fluent-dashboard-shadow); + border-radius: .75rem; + overflow: hidden; +} + +.fluent-dashboard__card--wide { + grid-column: 1 / -1; +} + +.fluent-dashboard__card-title { + color: inherit; + border-block-end: 1px solid var(--fluent-dashboard-border); + margin: 0; + padding: 1rem 1.1rem .85rem 1.35rem; + font-size: 1rem; + font-weight: 650; + line-height: 1.25; + position: relative; +} + +.fluent-dashboard__card-title:before { + content: ""; + background: highlight; + border-radius: 1rem; + width: .2rem; + position: absolute; + inset: 1rem auto .85rem .7rem; +} + +.fluent-dashboard__card-content { + padding: 1rem 1.1rem; +} + +.fluent-dashboard__details { + grid-template-columns: minmax(8rem, 36%) minmax(0, 1fr); + gap: .6rem 1rem; + margin: 0; + display: grid; +} + +.fluent-dashboard__details-label { + color: var(--fluent-dashboard-muted); +} + +.fluent-dashboard__details-value { + overflow-wrap: anywhere; + min-width: 0; + font-weight: 550; +} + +.fluent-dashboard__meter + .fluent-dashboard__meter { + margin-block-start: 1.1rem; +} + +.fluent-dashboard__meter-label { + justify-content: space-between; + gap: 1rem; + margin-block-end: .45rem; + font-size: .85rem; + display: flex; +} + +.fluent-dashboard__meter-label span:last-child { + color: var(--fluent-dashboard-muted); + font-variant-numeric: tabular-nums; +} + +.fluent-dashboard__meter-track { + background: color-mix(in srgb, currentColor 12%, transparent); + border-radius: 1rem; + height: .5rem; + overflow: hidden; +} + +.fluent-dashboard__meter-value { + border-radius: inherit; + background: highlight; + height: 100%; + transition: width .2s; + display: block; +} + +.fluent-dashboard__table-wrap { + max-width: 100%; + overflow-x: auto; +} + +.fluent-dashboard__table { + width: 100%; + color: inherit; + border-collapse: collapse; + margin: 0; +} + +.fluent-dashboard__table th, .fluent-dashboard__table td { + text-align: start; + vertical-align: middle; + border-block-end: 1px solid var(--fluent-dashboard-border); + padding: .7rem .65rem; +} + +.fluent-dashboard__table th { + color: var(--fluent-dashboard-muted); + letter-spacing: .02em; + text-transform: uppercase; + font-size: .78rem; + font-weight: 650; +} + +.fluent-dashboard__table tbody tr:last-child td { + border-block-end: 0; +} + +.fluent-dashboard__status { + white-space: nowrap; + align-items: center; + gap: .35rem; + font-size: .82rem; + font-weight: 600; + display: inline-flex; +} + +.fluent-dashboard__status:before { + content: ""; + background: currentColor; + border-radius: 50%; + width: .5rem; + height: .5rem; +} + +.fluent-dashboard__status--up { + color: color-mix(in srgb, LinkText 75%, Highlight); +} + +.fluent-dashboard__status--down { + color: color-mix(in srgb, currentColor 58%, transparent); +} + +.fluent-dashboard__table .fluent-dashboard__empty { + color: var(--fluent-dashboard-muted); + text-align: center; + padding-block: 1.5rem; + font-style: italic; +} + +@container (width <= 44rem) { + .fluent-dashboard__grid { + grid-template-columns: minmax(0, 1fr); + } + + .fluent-dashboard__card--wide { + grid-column: auto; + } +} + +@media (max-width: 575px) { + .fluent-dashboard__header { + flex-direction: column; + align-items: start; + padding: 1rem; + } + + .fluent-dashboard__details { + grid-template-columns: minmax(0, 1fr); + gap: .2rem; + } + + .fluent-dashboard__details-value + .fluent-dashboard__details-label { + margin-block-start: .45rem; + } + + .fluent-dashboard__card-content { + padding: .85rem; + } +} + +@media (prefers-reduced-motion: reduce) { + .fluent-dashboard__meter-value { + transition: none; + } +} diff --git a/luci-mod-fluentdashboard/htdocs/luci-static/resources/view/fluentdashboard/index.js b/luci-mod-fluentdashboard/htdocs/luci-static/resources/view/fluentdashboard/index.js new file mode 100644 index 000000000..a4b5b99e3 --- /dev/null +++ b/luci-mod-fluentdashboard/htdocs/luci-static/resources/view/fluentdashboard/index.js @@ -0,0 +1,654 @@ +"use strict"; +"require view"; +"require dom"; +"require poll"; +"require rpc"; +"require network"; +// WARN: generated by rsbuild - do not edit + +// The require scope +var __webpack_require__ = {}; + +// webpack/runtime/define_property_getters +(() => { +__webpack_require__.d = (exports, getters, values) => { + var define = (defs, kind) => { + for(var key in defs) { + if(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] }); + } + } + }; + define(getters, "get"); + define(values, "value"); +}; +})(); +// webpack/runtime/has_own_property +(() => { +__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +})(); +// webpack/runtime/make_namespace_object +(() => { +// define __esModule on exports +__webpack_require__.r = (exports) => { + if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { + Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + } + Object.defineProperty(exports, '__esModule', { value: true }); +}; +})(); +var __webpack_exports__ = {}; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + main: () => (/* binding */ main) +}); + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-factory.ts +const Fragment = Symbol.for("jsx.fragment"); +function jsx_factory_e(e, t) { + let { children: n, ...r } = null === t || "object" != typeof t || Array.isArray(t) ? {} : t, o = function e(t, n = []) { + for (let r of t)null != r && "boolean" != typeof r && (Array.isArray(r) ? e(r, n) : n.push(r instanceof Node ? r : String(r))); + return n; + }(null == n ? [] : Array.isArray(n) ? n : [ + n + ]); + if (e === Fragment) { + let e = document.createDocumentFragment(); + return e.append(...o), e; + } + if ("function" == typeof e) { + let t = Reflect.apply(e, void 0, [ + { + ...r, + children: o + } + ]); + if (!(t instanceof Node)) throw TypeError("JSX components must return a DOM Node"); + return t; + } + if ("string" != typeof e) throw TypeError("JSX element types must be tag names or component functions"); + let f = {}, l = { + ...r + }; + for (let [e, t] of Object.entries(l))e.startsWith("on") && "function" == typeof t ? (f[e] = t, delete l[e]) : "boolean" == typeof t && (t ? l[e] = e : delete l[e]); + let s = Object.keys(l).length > 0 ? o.length > 1 ? E(e, l, o) : E(e, l, o[0]) : o.length > 1 ? E(e, {}, o) : E(e, {}, o[0]); + for (let [e, t] of Object.entries(f)){ + let n = e.slice(2).toLowerCase(); + s.addEventListener(n, t); + } + return s; +} +function jsx(t, n) { + return jsx_factory_e(t, n); +} +function jsxs(t, n) { + return jsx_factory_e(t, n); +} +function jsxDEV(t, n) { + return jsx_factory_e(t, n); +} + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-runtime.ts + + +;// CONCATENATED MODULE: ./web/fluentdashboard/model.ts +function model_t(t) { + return null === t || "object" != typeof t || Array.isArray(t) ? {} : t; +} +function model_e(t) { + return "string" == typeof t ? t : ""; +} +function model_r(t) { + return "number" == typeof t && Number.isFinite(t) ? t : 0; +} +function model_o(e, i = "free") { + let n = model_t(e); + return { + total: model_r(n.total), + free: model_r(n[i] ?? n.free) + }; +} +function normalizeSystemSnapshot(i, n, a) { + let l = model_t(i), m = model_t(n), u = model_t(l.release), f = model_t(a), s = Array.isArray(m.load) ? m.load.map(model_r) : []; + return { + hostname: model_e(l.hostname), + model: model_e(l.model), + architecture: model_e(l.system), + target: model_e(u.target), + firmware: model_e(u.description), + kernel: model_e(l.kernel), + luciVersion: [ + model_e(f.branch), + model_e(f.revision) + ].filter(Boolean).join(" "), + uptime: model_r(m.uptime), + localtime: model_r(m.localtime), + load: s, + memory: model_o(m.memory, "available"), + swap: model_o(m.swap), + root: model_o(m.root), + tmp: model_o(m.tmp) + }; +} +function usedPercent(t) { + return t.total <= 0 ? 0 : Math.min(100, Math.max(0, (t.total - t.free) / t.total * 100)); +} +function formatBytes(t) { + if (!Number.isFinite(t) || t <= 0) return "0 B"; + let e = [ + "B", + "KiB", + "MiB", + "GiB", + "TiB" + ], r = Math.min(Math.floor(Math.log(t) / Math.log(1024)), e.length - 1), o = t / 1024 ** r; + return `${o >= 10 || 0 === r ? o.toFixed(0) : o.toFixed(1)} ${e[r]}`; +} +function formatDuration(t) { + if (!Number.isFinite(t) || t <= 0) return "-"; + let e = Math.floor(t / 86400), r = Math.floor(t % 86400 / 3600), o = Math.floor(t % 3600 / 60); + return [ + e > 0 ? `${e}d` : "", + `${r}h`, + `${o}m` + ].filter(Boolean).join(" "); +} +function formatLoad(t) { + return 0 === t.length ? "-" : t.slice(0, 3).map((t)=>(t / 65535).toFixed(2)).join(" / "); +} +function formatLocalTime(t) { + return !Number.isFinite(t) || t <= 0 ? "-" : new Intl.DateTimeFormat(void 0, { + dateStyle: "medium", + timeStyle: "medium", + timeZone: "UTC" + }).format(new Date(1000 * t)); +} + +;// CONCATENATED MODULE: ./web/fluentdashboard/index.tsx + + +let fluentdashboard_l = rpc.declare({ + object: "system", + method: "board" +}), fluentdashboard_o = rpc.declare({ + object: "system", + method: "info" +}), c = rpc.declare({ + object: "luci", + method: "getVersion" +}), fluentdashboard_u = rpc.declare({ + object: "luci-rpc", + method: "getDHCPLeases", + expect: { + "": {} + } +}); +function fluentdashboard_h(e) { + return null === e || "object" != typeof e || Array.isArray(e) ? {} : e; +} +function fluentdashboard_m(e) { + return "string" == typeof e ? e : ""; +} +function p(e) { + return "number" == typeof e && Number.isFinite(e) ? e : 0; +} +async function fluentdashboard_f(e, t) { + try { + return await e; + } catch { + return t; + } +} +function y(e, t = !1) { + let r = e.getL3Device() ?? e.getDevice(); + return { + name: e.getName(), + protocol: e.getI18n(), + up: e.isUp(), + addresses: t ? e.getIP6Addrs() : e.getIPAddrs(), + gateway: t ? e.getGateway6Addr() : e.getGatewayAddr(), + uptime: e.getUptime(), + device: r?.getName() ?? "", + rx: r?.getRXBytes() ?? 0, + tx: r?.getTXBytes() ?? 0 + }; +} +async function g() { + await fluentdashboard_f(network.flushCache(), {}); + let [e, t, r, a, s, n, d, u] = await Promise.all([ + fluentdashboard_f(fluentdashboard_l(), {}), + fluentdashboard_f(fluentdashboard_o(), {}), + fluentdashboard_f(c(), {}), + fluentdashboard_f(network.getWANNetworks(), []), + fluentdashboard_f(network.getWAN6Networks(), []), + fluentdashboard_f(network.getNetworks(), []), + fluentdashboard_f(network.getWifiDevices(), []), + fluentdashboard_f(network.getWifiNetworks(), []) + ]), h = u.map((e)=>{ + let t = d.find((t)=>t.getName() === e.getWifiDeviceName()); + return { + radio: t?.getName() ?? e.getWifiDeviceName() ?? "", + hardware: t?.getI18n() ?? "", + up: e.isUp() && !t?.isDisabled(), + ssid: e.getActiveSSID() || e.getSSID() || e.getMeshID() || "", + mode: e.getActiveModeI18n() || e.getMode(), + channel: e.getChannel(), + frequency: e.getFrequency() ?? "", + encryption: e.getActiveEncryption() + }; + }); + for (let e of d)h.some((t)=>t.radio === e.getName()) || h.push({ + radio: e.getName(), + hardware: e.getI18n(), + up: e.isUp() && !e.isDisabled(), + ssid: "", + mode: "", + channel: null, + frequency: "", + encryption: "" + }); + return { + system: normalizeSystemSnapshot(e, t, r), + wan4: a.map((e)=>y(e)), + wan6: s.map((e)=>y(e, !0)), + interfaces: n.filter((e)=>"loopback" !== e.getName()).map((e)=>y(e)), + wireless: h, + updatedAt: new Date() + }; +} +async function w() { + let [e, t, r] = await Promise.all([ + fluentdashboard_f(fluentdashboard_u(), {}), + fluentdashboard_f(network.getWifiNetworks(), []), + fluentdashboard_f(network.getHostHints(), null) + ]), a = await Promise.all(t.map(async (e)=>({ + wifi: e, + peers: await fluentdashboard_f(e.getAssocList(), []) + }))), s = []; + for (let { wifi: e, peers: t } of a)for (let a of t){ + let t = r?.getHostnameByMACAddr(a.mac) || r?.getIPAddrByMACAddr(a.mac) || r?.getIP6AddrByMACAddr(a.mac) || ""; + s.push({ + network: e.getActiveSSID() || e.getSSID() || e.getName(), + hostname: t, + mac: a.mac, + signal: a.signal, + noise: a.noise ?? null, + rxRate: a.rx?.rate ?? 0, + txRate: a.tx?.rate ?? 0 + }); + } + return { + leases: function(e) { + let t = fluentdashboard_h(e), r = []; + if (Array.isArray(t.dhcp_leases)) for (let e of t.dhcp_leases){ + let t = fluentdashboard_h(e); + r.push({ + family: "IPv4", + hostname: fluentdashboard_m(t.hostname), + address: fluentdashboard_m(t.ipaddr), + identifier: fluentdashboard_m(t.macaddr), + expires: !1 !== t.expires && p(t.expires) + }); + } + if (Array.isArray(t.dhcp6_leases)) for (let e of t.dhcp6_leases){ + let t = fluentdashboard_h(e), a = Array.isArray(t.ip6addrs) ? t.ip6addrs.filter((e)=>"string" == typeof e) : []; + r.push({ + family: "IPv6", + hostname: fluentdashboard_m(t.hostname), + address: a.join(", ") || fluentdashboard_m(t.ip6addr), + identifier: fluentdashboard_m(t.duid), + expires: !1 !== t.expires && p(t.expires) + }); + } + return r; + }(e), + clients: s + }; +} +function b(e) { + return null == e || "" === e ? "-" : String(e); +} +function v(t) { + return jsx("span", { + class: `fluent-dashboard__status fluent-dashboard__status--${t ? "up" : "down"}`, + children: t ? _("Online") : _("Offline") + }); +} +function A(r, a, s, n = !1) { + return jsxs("div", { + class: `fluent-dashboard__card${n ? " fluent-dashboard__card--wide" : ""}`, + "data-dashboard-section": r, + children: [ + jsx("h2", { + class: "fluent-dashboard__card-title", + children: a + }), + jsx("div", { + class: "fluent-dashboard__card-content", + children: s + }) + ] + }); +} +function S(r, a, s) { + return jsx("div", { + class: "fluent-dashboard__table-wrap", + children: jsxs("table", { + class: "fluent-dashboard__table", + children: [ + jsx("thead", { + children: jsx("tr", { + children: r.map((t)=>jsx("th", { + children: t + })) + }) + }), + jsx("tbody", { + children: a.length ? a.map((t)=>jsx("tr", { + children: t.map((t)=>jsx("td", { + children: t instanceof Node ? t : b(t) + })) + })) : jsx("tr", { + children: jsx("td", { + class: "fluent-dashboard__empty", + colSpan: r.length, + children: s + }) + }) + }) + ] + }) + }); +} +function x(t) { + return A("system", _("System"), jsx("div", { + class: "fluent-dashboard__details", + children: [ + [ + _("Hostname"), + t.hostname + ], + [ + _("Model"), + t.model + ], + [ + _("Architecture"), + t.architecture + ], + [ + _("Target Platform"), + t.target + ], + [ + _("Firmware Version"), + t.firmware + ], + [ + _("Kernel Version"), + t.kernel + ], + [ + _("LuCI Version"), + t.luciVersion + ], + [ + _("Local Time"), + formatLocalTime(t.localtime) + ], + [ + _("Uptime"), + formatDuration(t.uptime) + ], + [ + _("Load Average"), + formatLoad(t.load) + ] + ].flatMap(([t, r])=>[ + jsx("div", { + class: "fluent-dashboard__details-label", + children: t + }), + jsx("div", { + class: "fluent-dashboard__details-value", + children: r instanceof Node ? r : b(r) + }) + ]) + })); +} +function I(a) { + let s = [ + [ + _("Memory"), + a.memory + ], + [ + _("Swap"), + a.swap + ], + [ + _("Root storage"), + a.root + ], + [ + _("Temporary storage"), + a.tmp + ] + ]; + return A("resources", _("Resources"), s.map(([a, s])=>{ + let n, i; + return n = Math.max(0, s.total - s.free), i = usedPercent(s), jsxs("div", { + class: "fluent-dashboard__meter", + children: [ + jsxs("div", { + class: "fluent-dashboard__meter-label", + children: [ + jsx("span", { + children: a + }), + jsx("span", { + children: `${formatBytes(n)} / ${formatBytes(s.total)}` + }) + ] + }), + jsx("div", { + class: "fluent-dashboard__meter-track", + role: "progressbar", + "aria-valuemin": 0, + "aria-valuemax": 100, + "aria-valuenow": Math.round(i), + children: jsx("span", { + class: "fluent-dashboard__meter-value", + style: `width:${i.toFixed(1)}%` + }) + }) + ] + }); + })); +} +function N(e) { + let t = [ + ...e.wan4.map((e)=>[ + "IPv4", + e + ]), + ...e.wan6.map((e)=>[ + "IPv6", + e + ]) + ]; + return A("internet", _("Internet"), S([ + _("Family"), + _("Interface"), + _("Status"), + _("Protocol"), + _("Address"), + _("Gateway"), + _("Uptime") + ], t.map(([e, t])=>[ + e, + t.name, + v(t.up), + t.protocol, + t.addresses.join(", "), + t.gateway, + formatDuration(t.uptime) + ]), _("No Internet connection information is available.")), !0); +} +function D(e) { + return A("interfaces", _("Interfaces"), S([ + _("Interface"), + _("Device"), + _("Status"), + _("Address"), + _("Received"), + _("Transmitted") + ], e.map((e)=>[ + e.name, + e.device, + v(e.up), + e.addresses.join(", "), + formatBytes(e.rx), + formatBytes(e.tx) + ]), _("No network interfaces were found.")), !0); +} +function k(t) { + return A("wireless", _("Wireless"), S([ + _("Radio"), + _("SSID"), + _("Status"), + _("Mode"), + _("Channel"), + _("Encryption") + ], t.map((t)=>[ + jsx("span", { + title: t.hardware, + children: b(t.radio) + }), + t.ssid, + v(t.up), + t.mode, + null === t.channel ? "-" : t.frequency ? `${t.channel} (${t.frequency} GHz)` : t.channel, + t.encryption + ]), _("No wireless radios were found.")), !0); +} +function C(e) { + return A("leases", _("Active DHCP Leases"), S([ + _("Family"), + _("Hostname"), + _("Address"), + _("MAC / DUID"), + _("Expires") + ], e.map((e)=>{ + var t; + return [ + e.family, + e.hostname, + e.address, + e.identifier, + !1 === (t = e.expires) ? _("Unlimited") : t <= 0 ? _("Expired") : formatDuration(t) + ]; + }), _("There are no active DHCP leases.")), !0); +} +function P(e) { + return e > 0 ? `${(e / 1000).toFixed(1)} ${_("Mbit/s")}` : "-"; +} +function M(e) { + return A("clients", _("Wireless Clients"), S([ + _("Network"), + _("Host"), + _("MAC address"), + _("Signal"), + _("RX / TX Rate") + ], e.map((e)=>[ + e.network, + e.hostname, + e.mac, + null === e.noise ? `${e.signal} dBm` : `${e.signal} / ${e.noise} dBm`, + `${P(e.rxRate)} / ${P(e.txRate)}` + ]), _("No wireless clients are connected.")), !0); +} +class $ extends L.view { + handleSave = null; + handleSaveApply = null; + handleReset = null; + root = null; + load() { + return Promise.all([ + g(), + w() + ]); + } + replaceSection(e, t) { + this.root?.querySelector(`[data-dashboard-section="${e}"]`)?.replaceWith(t); + } + async refreshSummary() { + if (!this.root?.isConnected) return; + let e = await g(); + if (!this.root?.isConnected) return; + this.replaceSection("system", x(e.system)), this.replaceSection("resources", I(e.system)), this.replaceSection("internet", N(e)), this.replaceSection("interfaces", D(e.interfaces)), this.replaceSection("wireless", k(e.wireless)); + let t = this.root.querySelector("[data-dashboard-updated]"); + t && (t.textContent = e.updatedAt.toLocaleTimeString()); + } + async refreshDetails() { + if (!this.root?.isConnected) return; + let e = await w(); + this.root?.isConnected && (this.replaceSection("leases", C(e.leases)), this.replaceSection("clients", M(e.clients))); + } + render([r, a]) { + return document.getElementById("fluent-dashboard-styles") || document.head.appendChild(jsx("link", { + id: "fluent-dashboard-styles", + rel: "stylesheet", + href: L.resource("view/fluentdashboard/index.css") + })), this.root = jsxs("div", { + class: "fluent-dashboard", + children: [ + jsxs("div", { + class: "fluent-dashboard__header", + children: [ + jsxs("div", { + children: [ + jsx("h1", { + children: _("Fluent Dashboard") + }), + jsx("p", { + children: _("A live overview of this OpenWrt device.") + }) + ] + }), + jsxs("p", { + class: "fluent-dashboard__updated", + children: [ + _("Updated"), + " ", + jsx("time", { + "data-dashboard-updated": "", + children: r.updatedAt.toLocaleTimeString() + }) + ] + }) + ] + }), + jsxs("div", { + class: "fluent-dashboard__grid", + children: [ + x(r.system), + I(r.system), + N(r), + D(r.interfaces), + k(r.wireless), + C(a.leases), + M(a.clients) + ] + }) + ] + }), poll.add(()=>this.refreshSummary(), 5), poll.add(()=>this.refreshDetails(), 15), this.root; + } +} +const main = $; + + +return main; \ No newline at end of file diff --git a/luci-mod-fluentdashboard/po/templates/fluentdashboard.pot b/luci-mod-fluentdashboard/po/templates/fluentdashboard.pot new file mode 100644 index 000000000..b8efbc58b --- /dev/null +++ b/luci-mod-fluentdashboard/po/templates/fluentdashboard.pot @@ -0,0 +1,177 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-mod-fluentdashboard\n" +"POT-Creation-Date: 2026-08-24 00:20+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \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" + +msgid "A live overview of this OpenWrt device." +msgstr "" + +msgid "Active DHCP Leases" +msgstr "" + +msgid "Address" +msgstr "" + +msgid "Architecture" +msgstr "" + +msgid "Channel" +msgstr "" + +msgid "Device" +msgstr "" + +msgid "Encryption" +msgstr "" + +msgid "Expired" +msgstr "" + +msgid "Expires" +msgstr "" + +msgid "Family" +msgstr "" + +msgid "Firmware Version" +msgstr "" + +msgid "Fluent Dashboard" +msgstr "" + +msgid "Gateway" +msgstr "" + +msgid "Host" +msgstr "" + +msgid "Hostname" +msgstr "" + +msgid "Interface" +msgstr "" + +msgid "Interfaces" +msgstr "" + +msgid "Internet" +msgstr "" + +msgid "Kernel Version" +msgstr "" + +msgid "Load Average" +msgstr "" + +msgid "Local Time" +msgstr "" + +msgid "LuCI Version" +msgstr "" + +msgid "MAC / DUID" +msgstr "" + +msgid "MAC address" +msgstr "" + +msgid "Mbit/s" +msgstr "" + +msgid "Memory" +msgstr "" + +msgid "Mode" +msgstr "" + +msgid "Model" +msgstr "" + +msgid "Network" +msgstr "" + +msgid "No Internet connection information is available." +msgstr "" + +msgid "No network interfaces were found." +msgstr "" + +msgid "No wireless clients are connected." +msgstr "" + +msgid "No wireless radios were found." +msgstr "" + +msgid "Offline" +msgstr "" + +msgid "Online" +msgstr "" + +msgid "Protocol" +msgstr "" + +msgid "Radio" +msgstr "" + +msgid "Received" +msgstr "" + +msgid "Resources" +msgstr "" + +msgid "Root storage" +msgstr "" + +msgid "RX / TX Rate" +msgstr "" + +msgid "Signal" +msgstr "" + +msgid "SSID" +msgstr "" + +msgid "Status" +msgstr "" + +msgid "Swap" +msgstr "" + +msgid "System" +msgstr "" + +msgid "Target Platform" +msgstr "" + +msgid "Temporary storage" +msgstr "" + +msgid "There are no active DHCP leases." +msgstr "" + +msgid "Transmitted" +msgstr "" + +msgid "Unlimited" +msgstr "" + +msgid "Updated" +msgstr "" + +msgid "Uptime" +msgstr "" + +msgid "Wireless" +msgstr "" + +msgid "Wireless Clients" +msgstr "" diff --git a/luci-mod-fluentdashboard/po/zh-cn b/luci-mod-fluentdashboard/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-mod-fluentdashboard/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-mod-fluentdashboard/po/zh_Hans/fluentdashboard.po b/luci-mod-fluentdashboard/po/zh_Hans/fluentdashboard.po new file mode 100644 index 000000000..1aab7d995 --- /dev/null +++ b/luci-mod-fluentdashboard/po/zh_Hans/fluentdashboard.po @@ -0,0 +1,177 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-mod-fluentdashboard\n" +"POT-Creation-Date: 2026-08-24 00:20+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: LazuliKao\n" +"Language-Team: LuCI Fluent Dashboard\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" + +msgid "A live overview of this OpenWrt device." +msgstr "此 OpenWrt 设备的实时概览。" + +msgid "Active DHCP Leases" +msgstr "活动的 DHCP 租约" + +msgid "Address" +msgstr "地址" + +msgid "Architecture" +msgstr "架构" + +msgid "Channel" +msgstr "信道" + +msgid "Device" +msgstr "设备" + +msgid "Encryption" +msgstr "加密" + +msgid "Expired" +msgstr "已过期" + +msgid "Expires" +msgstr "到期时间" + +msgid "Family" +msgstr "协议族" + +msgid "Firmware Version" +msgstr "固件版本" + +msgid "Fluent Dashboard" +msgstr "Fluent 状态概览" + +msgid "Gateway" +msgstr "网关" + +msgid "Host" +msgstr "主机" + +msgid "Hostname" +msgstr "主机名" + +msgid "Interface" +msgstr "接口" + +msgid "Interfaces" +msgstr "接口" + +msgid "Internet" +msgstr "互联网" + +msgid "Kernel Version" +msgstr "内核版本" + +msgid "Load Average" +msgstr "平均负载" + +msgid "Local Time" +msgstr "本地时间" + +msgid "LuCI Version" +msgstr "LuCI 版本" + +msgid "MAC / DUID" +msgstr "MAC / DUID" + +msgid "MAC address" +msgstr "MAC 地址" + +msgid "Mbit/s" +msgstr "Mbit/s" + +msgid "Memory" +msgstr "内存" + +msgid "Mode" +msgstr "模式" + +msgid "Model" +msgstr "型号" + +msgid "Network" +msgstr "网络" + +msgid "No Internet connection information is available." +msgstr "没有可用的互联网连接信息。" + +msgid "No network interfaces were found." +msgstr "未找到网络接口。" + +msgid "No wireless clients are connected." +msgstr "没有已连接的无线客户端。" + +msgid "No wireless radios were found." +msgstr "未找到无线射频。" + +msgid "Offline" +msgstr "离线" + +msgid "Online" +msgstr "在线" + +msgid "Protocol" +msgstr "协议" + +msgid "Radio" +msgstr "射频" + +msgid "Received" +msgstr "已接收" + +msgid "Resources" +msgstr "资源" + +msgid "Root storage" +msgstr "根存储" + +msgid "RX / TX Rate" +msgstr "接收 / 发送速率" + +msgid "Signal" +msgstr "信号" + +msgid "SSID" +msgstr "SSID" + +msgid "Status" +msgstr "状态" + +msgid "Swap" +msgstr "交换空间" + +msgid "System" +msgstr "系统" + +msgid "Target Platform" +msgstr "目标平台" + +msgid "Temporary storage" +msgstr "临时存储" + +msgid "There are no active DHCP leases." +msgstr "没有活动的 DHCP 租约。" + +msgid "Transmitted" +msgstr "已发送" + +msgid "Unlimited" +msgstr "无限制" + +msgid "Updated" +msgstr "更新时间" + +msgid "Uptime" +msgstr "运行时间" + +msgid "Wireless" +msgstr "无线" + +msgid "Wireless Clients" +msgstr "无线客户端" diff --git a/luci-mod-fluentdashboard/root/usr/share/luci/menu.d/luci-mod-fluentdashboard.json b/luci-mod-fluentdashboard/root/usr/share/luci/menu.d/luci-mod-fluentdashboard.json new file mode 100644 index 000000000..05710bc83 --- /dev/null +++ b/luci-mod-fluentdashboard/root/usr/share/luci/menu.d/luci-mod-fluentdashboard.json @@ -0,0 +1,13 @@ +{ + "admin/fluentdashboard": { + "title": "Fluent Dashboard", + "order": 1, + "action": { + "type": "view", + "path": "fluentdashboard/index" + }, + "depends": { + "acl": [ "luci-mod-fluentdashboard-index" ] + } + } +} diff --git a/luci-mod-fluentdashboard/root/usr/share/rpcd/acl.d/luci-mod-fluentdashboard.json b/luci-mod-fluentdashboard/root/usr/share/rpcd/acl.d/luci-mod-fluentdashboard.json new file mode 100644 index 000000000..39d80d304 --- /dev/null +++ b/luci-mod-fluentdashboard/root/usr/share/rpcd/acl.d/luci-mod-fluentdashboard.json @@ -0,0 +1,13 @@ +{ + "luci-mod-fluentdashboard-index": { + "description": "Grant access to the Fluent dashboard status display", + "read": { + "ubus": { + "iwinfo": [ "assoclist" ], + "luci": [ "getVersion" ], + "luci-rpc": [ "getDHCPLeases" ], + "system": [ "board", "info" ] + } + } + } +} diff --git a/luci-theme-fluent-lite/Makefile b/luci-theme-fluent-lite/Makefile new file mode 100644 index 000000000..1860ca61b --- /dev/null +++ b/luci-theme-fluent-lite/Makefile @@ -0,0 +1,56 @@ +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Fluent Theme for LuCI (lite) +LUCI_DEPENDS:=+luci-base +LUCI_MINIFY_CSS:=0 + +PKG_NAME:=luci-theme-fluent-lite +PKG_VERSION:=1.0.9 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 + +FLUENT_LITE_SOURCE_DIR:=$(CURDIR) +FLUENT_FULL_SOURCE_DIR:=$(abspath $(CURDIR)/../luci-theme-fluent) + +define LUCI_DESCRIPTION +Core FluentUI theme styling for LuCI without optional menu, updater, log, and application enhancements. +endef + +define Build/Prepare/$(PKG_NAME) + $(CP) $(FLUENT_FULL_SOURCE_DIR)/htdocs/* $(PKG_BUILD_DIR)/htdocs/ + $(CP) $(FLUENT_FULL_SOURCE_DIR)/root/* $(PKG_BUILD_DIR)/root/ + $(CP) $(FLUENT_LITE_SOURCE_DIR)/htdocs/* $(PKG_BUILD_DIR)/htdocs/ + $(CP) $(FLUENT_LITE_SOURCE_DIR)/root/* $(PKG_BUILD_DIR)/root/ +endef + +define Package/$(PKG_NAME)/conffiles +/etc/config/fluent +endef + +define Package/$(PKG_NAME)/preinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + if command -v apk >/dev/null 2>&1 && apk info -e luci-theme-fluent >/dev/null 2>&1; then + echo "luci-theme-fluent is installed; remove it before installing luci-theme-fluent-lite" >&2 + exit 1 + fi + if command -v opkg >/dev/null 2>&1 && opkg list-installed luci-theme-fluent 2>/dev/null | grep -q '^luci-theme-fluent '; then + echo "luci-theme-fluent is installed; remove it before installing luci-theme-fluent-lite" >&2 + exit 1 + fi +} +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + uci batch <<-EOF + set luci.themes.Fluent=/luci-static/fluent + commit luci + EOF + /etc/init.d/rpcd restart +} +endef + +# call BuildPackage - OpenWrt buildroot signature +include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-theme-fluent-lite/htdocs/luci-static/resources/menu-fluent.js b/luci-theme-fluent-lite/htdocs/luci-static/resources/menu-fluent.js new file mode 100644 index 000000000..aa5d24f4f --- /dev/null +++ b/luci-theme-fluent-lite/htdocs/luci-static/resources/menu-fluent.js @@ -0,0 +1,1060 @@ +"use strict"; +"require baseclass"; +"require ui"; +"require dom"; +// WARN: generated by rsbuild - do not edit + +// The require scope +var __webpack_require__ = {}; + +// webpack/runtime/define_property_getters +(() => { +__webpack_require__.d = (exports, getters, values) => { + var define = (defs, kind) => { + for(var key in defs) { + if(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] }); + } + } + }; + define(getters, "get"); + define(values, "value"); +}; +})(); +// webpack/runtime/has_own_property +(() => { +__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +})(); +// webpack/runtime/make_namespace_object +(() => { +// define __esModule on exports +__webpack_require__.r = (exports) => { + if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { + Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + } + Object.defineProperty(exports, '__esModule', { value: true }); +}; +})(); +var __webpack_exports__ = {}; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + main: () => (/* binding */ main) +}); + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-factory.ts +const Fragment = Symbol.for("jsx.fragment"); +function jsx_factory_e(e, t) { + let { children: n, ...r } = null === t || "object" != typeof t || Array.isArray(t) ? {} : t, o = function e(t, n = []) { + for (let r of t)null != r && "boolean" != typeof r && (Array.isArray(r) ? e(r, n) : n.push(r instanceof Node ? r : String(r))); + return n; + }(null == n ? [] : Array.isArray(n) ? n : [ + n + ]); + if (e === Fragment) { + let e = document.createDocumentFragment(); + return e.append(...o), e; + } + if ("function" == typeof e) { + let t = Reflect.apply(e, void 0, [ + { + ...r, + children: o + } + ]); + if (!(t instanceof Node)) throw TypeError("JSX components must return a DOM Node"); + return t; + } + if ("string" != typeof e) throw TypeError("JSX element types must be tag names or component functions"); + let f = {}, l = { + ...r + }; + for (let [e, t] of Object.entries(l))e.startsWith("on") && "function" == typeof t ? (f[e] = t, delete l[e]) : "boolean" == typeof t && (t ? l[e] = e : delete l[e]); + let s = Object.keys(l).length > 0 ? o.length > 1 ? E(e, l, o) : E(e, l, o[0]) : o.length > 1 ? E(e, {}, o) : E(e, {}, o[0]); + for (let [e, t] of Object.entries(f)){ + let n = e.slice(2).toLowerCase(); + s.addEventListener(n, t); + } + return s; +} +function jsx(t, n) { + return jsx_factory_e(t, n); +} +function jsxs(t, n) { + return jsx_factory_e(t, n); +} +function jsxDEV(t, n) { + return jsx_factory_e(t, n); +} + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-runtime.ts + + +;// CONCATENATED MODULE: ./web/resources/menu-layout.ts +let menu_layout_e = "primary:", menu_layout_t = new Set([ + "version", + "custom", + "titles", + "itemTitles", + "categoryMoves", + "itemMoves", + "hiddenCategories", + "hiddenItems" +]); +function menu_layout_n(e) { + return "function" == typeof _ ? _(e) : e; +} +function primaryCategoryId(t) { + return `${menu_layout_e}${t}`; +} +function menu_layout_i(t) { + return t.startsWith(menu_layout_e) ? t.slice(menu_layout_e.length) : null; +} +function menu_layout_r(e) { + let t = [], i = []; + for (let r of ui.menu.getChildren(e))if (r.satisfied) for (let e of ui.menu.getChildren(r)){ + let l = e.title?.trim(); + if (!e.satisfied || !l) continue; + let o = `${r.name}/${e.name}`, u = menu_layout_n(l); + for (let a of (t.push({ + path: o, + pathSegments: [ + r.name, + e.name + ], + node: e, + title: u, + rawTitle: l + }), ui.menu.getChildren(e))){ + let t = a.title?.trim(); + a.satisfied && t && i.push({ + path: `${o}/${a.name}`, + pathSegments: [ + r.name, + e.name, + a.name + ], + node: a, + title: menu_layout_n(t), + rawTitle: t, + originalPrimaryPath: o, + originalPrimaryTitle: u + }); + } + } + return { + primaries: t, + items: i + }; +} +function discoverMenuItems(e) { + return menu_layout_r(e).items; +} +function menu_layout_l(e) { + let t = e.primaries.map((e)=>({ + id: primaryCategoryId(e.path), + title: e.title, + items: [] + })), n = new Map(t.map((e)=>[ + e.id, + e + ])); + for (let t of e.items)n.get(primaryCategoryId(t.originalPrimaryPath))?.items.push(t.path); + return t; +} +function buildDefaultMenuCategories(e) { + return menu_layout_l(menu_layout_r(e)); +} +function menu_layout_o(e) { + return e.map((e)=>({ + ...e, + items: [ + ...e.items + ] + })); +} +function moveMenuItem(e, t, n, i) { + let r = menu_layout_o(e), l = r.find((e)=>e.id === n); + if (!l || i === t) return r; + for (let e of r)e.items = e.items.filter((e)=>e !== t); + let u = i ? l.items.indexOf(i) : -1; + return u >= 0 ? l.items.splice(u, 0, t) : l.items.push(t), r; +} +function moveMenuCategory(e, t, n, i = "before") { + let r = menu_layout_o(e), l = r.findIndex((e)=>e.id === t); + if (l < 0 || t === n || !r.some((e)=>e.id === n)) return r; + let [u] = r.splice(l, 1), a = r.findIndex((e)=>e.id === n); + return r.splice("after" === i ? a + 1 : a, 0, u), r; +} +function menu_layout_u(e, t) { + let n = e[t]; + return new Set(e.slice(t + 1).filter((e)=>e.originalPrimaryPath === n.originalPrimaryPath).map((e)=>e.path)); +} +function canRestoreMenuItem(e, t, n) { + let i = t.findIndex((e)=>e.path === n); + if (i < 0) return !1; + let r = primaryCategoryId(t[i].originalPrimaryPath), l = e.find((e)=>e.id === r); + if (!l) return !0; + let o = 0, a = -1; + for (let t of e)for(let e = 0; e < t.items.length; e += 1)t.items[e] === n && (o += 1, t.id === r && (a = e)); + if (1 !== o || a < 0) return !0; + let s = menu_layout_u(t, i), f = l.items.findIndex((e)=>s.has(e)); + return (f < 0 ? l.items.length - 1 : f > a ? f - 1 : f) !== a; +} +function restoreMenuItemToOriginalPosition(e, t, n) { + let i = t.findIndex((e)=>e.path === n); + if (i < 0) return menu_layout_o(e); + let r = t[i], l = primaryCategoryId(r.originalPrimaryPath), a = e.some((e)=>e.id === l) ? e : [ + ...e, + { + id: l, + title: r.originalPrimaryTitle, + items: [] + } + ], s = menu_layout_u(t, i), f = a.find((e)=>e.id === l); + return moveMenuItem(a, n, l, f?.items.find((e)=>s.has(e))); +} +function menu_layout_a(e, t) { + if ("string" != typeof e || e !== e.trim()) return !1; + let n = e.split("/"); + return n.length === t && n.every(Boolean); +} +function menu_layout_s(e) { + return "number" == typeof e ? `custom:${e}` : `primary:${e}`; +} +function menu_layout_f(e, t) { + return Number.isInteger(e) && e >= 0 && e < t || menu_layout_a(e, 2) ? e : null; +} +function menu_layout_d(e, t) { + let n = e[t]; + return void 0 === n ? [] : Array.isArray(n) ? n : null; +} +function parseMenuLayout(e) { + let n; + if ("string" != typeof e || "" === e.trim()) return null; + try { + n = JSON.parse(e); + } catch { + return null; + } + if (!n || "object" != typeof n || Array.isArray(n)) return null; + let i = n; + if (1 !== i.version || Object.keys(i).some((e)=>!menu_layout_t.has(e))) return null; + let r = menu_layout_d(i, "custom"), l = menu_layout_d(i, "titles"), o = menu_layout_d(i, "itemTitles"), u = menu_layout_d(i, "categoryMoves"), m = menu_layout_d(i, "itemMoves"), p = menu_layout_d(i, "hiddenCategories"), h = menu_layout_d(i, "hiddenItems"); + if (!r || !l || !o || !u || !m || !p || !h) return null; + let g = [], y = new Set(); + for (let e of r){ + if ("string" != typeof e) return null; + let t = e.trim(), n = t.toLocaleLowerCase(); + if (!t || y.has(n)) return null; + y.add(n), g.push(t); + } + let c = [], M = new Set(); + for (let e of l){ + if (!Array.isArray(e) || 2 !== e.length || !menu_layout_a(e[0], 2) || "string" != typeof e[1]) return null; + let t = e[1].trim(); + if (!t || M.has(e[0])) return null; + M.add(e[0]), c.push([ + e[0], + t + ]); + } + let v = [], w = new Set(); + for (let e of o){ + if (!Array.isArray(e) || 2 !== e.length || !menu_layout_a(e[0], 3) || "string" != typeof e[1]) return null; + let t = e[1].trim(); + if (!t || w.has(e[0])) return null; + w.add(e[0]), v.push([ + e[0], + t + ]); + } + let I = [], C = new Set(); + for (let e of u){ + if (!Array.isArray(e) || 2 !== e.length) return null; + let t = menu_layout_f(e[0], g.length), n = null === e[1] ? null : menu_layout_f(e[1], g.length); + if (null === t || null !== e[1] && null === n) return null; + let i = menu_layout_s(t); + if (C.has(i) || null !== n && i === menu_layout_s(n)) return null; + C.add(i), I.push([ + t, + n + ]); + } + let S = [], x = new Set(); + for (let e of m){ + if (!Array.isArray(e) || 3 !== e.length || !menu_layout_a(e[0], 3)) return null; + let t = menu_layout_f(e[1], g.length), n = null === e[2] ? null : menu_layout_a(e[2], 3) ? e[2] : null; + if (null === t || null !== e[2] && null === n || x.has(e[0]) || n === e[0]) return null; + x.add(e[0]), S.push([ + e[0], + t, + n + ]); + } + let P = [], A = new Set(); + for (let e of p){ + let t = menu_layout_f(e, g.length); + if (null === t || A.has(menu_layout_s(t))) return null; + A.add(menu_layout_s(t)), P.push(t); + } + let T = [], b = new Set(); + for (let e of h){ + if (!menu_layout_a(e, 3) || b.has(e)) return null; + b.add(e), T.push(e); + } + return { + version: 1, + custom: g, + titles: c, + itemTitles: v, + categoryMoves: I, + itemMoves: S, + hiddenCategories: P, + hiddenItems: T + }; +} +function menu_layout_m() { + return { + titles: [], + itemTitles: [], + categoryMoves: [], + itemMoves: [] + }; +} +function menu_layout_p(e, t) { + return "number" == typeof e ? t[e] ?? null : primaryCategoryId(e); +} +function resolveMenuLayout(e, t) { + let n = menu_layout_r(e), i = menu_layout_l(n), u = new Set(), a = new Set(), s = menu_layout_m(); + if (!t) return { + categories: i, + hiddenCategoryIds: u, + hiddenItemPaths: a, + itemTitles: new Map(), + pending: s, + configured: !1 + }; + let f = t.custom.map((e, t)=>{ + let n = `custom:${t}`; + return i.push({ + id: n, + title: e, + items: [] + }), n; + }), d = new Set(n.items.map((e)=>e.path)), h = f.length > 0; + for (let [e, n] of t.titles){ + let t = primaryCategoryId(e), r = i.find((e)=>e.id === t); + r ? (r.title = n, h = !0) : s.titles.push([ + t, + n + ]); + } + let g = new Map(); + for (let [e, n] of t.itemTitles)d.has(e) ? (g.set(e, n), h = !0) : s.itemTitles.push([ + e, + n + ]); + for (let [e, n] of t.categoryMoves){ + let t = menu_layout_p(e, f), r = null === n ? null : menu_layout_p(n, f), l = null !== t && i.some((e)=>e.id === t), u = null === r || i.some((e)=>e.id === r); + t && l && u ? (i = function(e, t, n) { + if (n) return moveMenuCategory(e, t, n); + let i = menu_layout_o(e), r = i.findIndex((e)=>e.id === t); + if (r < 0) return i; + let [l] = i.splice(r, 1); + return i.push(l), i; + }(i, t, r), h = !0) : t && s.categoryMoves.push([ + t, + r + ]); + } + for (let [e, n, r] of t.itemMoves){ + let t = menu_layout_p(n, f), l = t ? i.find((e)=>e.id === t) : void 0, o = null === r || !!l?.items.includes(r); + d.has(e) && t && l && o ? (i = moveMenuItem(i, e, t, r ?? void 0), h = !0) : t && s.itemMoves.push([ + e, + t, + r + ]); + } + for (let e of t.hiddenCategories){ + let t = menu_layout_p(e, f); + t && (u.add(t), i.some((e)=>e.id === t) && (h = !0)); + } + for (let e of t.hiddenItems)a.add(e), d.has(e) && (h = !0); + return { + categories: i, + hiddenCategoryIds: u, + hiddenItemPaths: a, + itemTitles: g, + pending: s, + configured: h + }; +} +function menu_layout_h(e, t) { + let n = new Map(t.map((e, t)=>[ + e, + t + ])), i = e.flatMap((e)=>{ + let t = n.get(e); + return void 0 === t ? [] : [ + { + value: e, + index: t + } + ]; + }), r = [], l = Array(i.length).fill(-1); + for(let e = 0; e < i.length; e += 1){ + let t = 0, n = r.length; + for(; t < n;){ + let l = t + n >> 1; + i[r[l]].index < i[e].index ? t = l + 1 : n = l; + } + t > 0 && (l[e] = r[t - 1]), r[t] = e; + } + let o = new Set(); + for(let e = r.at(-1) ?? -1; e >= 0; e = l[e])o.add(i[e].value); + return o; +} +function menu_layout_g(e, t) { + let n = menu_layout_i(e); + return n || (t.get(e) ?? null); +} +function menu_layout_y(e, t, n) { + let i = n.get(e), r = n.get(t); + return void 0 !== i && void 0 !== r ? i - r : void 0 !== i ? -1 : void 0 !== r ? 1 : e.localeCompare(t); +} +function serializeMenuLayout(e, t, n, o, u = menu_layout_m(), a = new Map()) { + let s = menu_layout_r(e), f = menu_layout_l(s), d = new Map(f.map((e)=>[ + e.id, + e + ])), p = t.filter((e)=>!d.has(e.id)), c = new Map(p.map((e, t)=>[ + e.id, + t + ])), M = p.map((e)=>e.title.trim()), v = [], w = new Set(); + for (let e of t){ + let t = d.get(e.id), n = e.title.trim(); + if (!t || n === t.title) continue; + let r = menu_layout_i(e.id); + r && (v.push([ + r, + n + ]), w.add(e.id)); + } + for (let [e, t] of u.titles){ + if (w.has(e)) continue; + let n = menu_layout_i(e); + n && v.push([ + n, + t + ]); + } + let I = new Map(s.items.map((e)=>[ + e.path, + e.title + ])), C = [], S = new Set(); + for (let [e, t] of a){ + let n = t.trim(); + n && n !== I.get(e) && (C.push([ + e, + n + ]), S.add(e)); + } + for (let [e, t] of u.itemTitles)S.has(e) || C.push([ + e, + t + ]); + let x = [ + ...f.map((e)=>e.id), + ...p.map((e)=>e.id) + ], P = t.map((e)=>e.id), A = [], T = new Set(); + for (let [e, t] of function(e, t) { + let n = menu_layout_h(e, t), i = []; + for(let e = t.length - 1; e >= 0; e -= 1)n.has(t[e]) || i.push([ + t[e], + t[e + 1] ?? null + ]); + return i; + }(x, P)){ + let n = menu_layout_g(e, c), i = null === t ? null : menu_layout_g(t, c); + null !== n && (null === t || null !== i) && (A.push([ + n, + i + ]), T.add(e)); + } + for (let [e, t] of u.categoryMoves){ + if (T.has(e)) continue; + let n = menu_layout_g(e, c), i = null === t ? null : menu_layout_g(t, c); + null !== n && (null === t || null !== i) && A.push([ + n, + i + ]); + } + let b = [], $ = new Set(); + for (let e of t){ + let t = menu_layout_g(e.id, c); + if (null === t) continue; + let n = menu_layout_h(d.get(e.id)?.items ?? [], e.items); + for(let i = e.items.length - 1; i >= 0; i -= 1){ + let r = e.items[i]; + n.has(r) || (b.push([ + r, + t, + e.items[i + 1] ?? null + ]), $.add(r)); + } + } + for (let [e, t, n] of u.itemMoves){ + if ($.has(e)) continue; + let i = menu_layout_g(t, c); + null !== i && b.push([ + e, + i, + n + ]); + } + let L = new Map(t.map((e, t)=>[ + e.id, + t + ])), O = [ + ...n + ].sort((e, t)=>menu_layout_y(e, t, L)).flatMap((e)=>{ + let t = menu_layout_g(e, c); + return null === t ? [] : [ + t + ]; + }), j = new Map(s.items.map((e, t)=>[ + e.path, + t + ])), N = [ + ...o + ].sort((e, t)=>menu_layout_y(e, t, j)), k = { + version: 1 + }; + return M.length > 0 && (k.custom = M), v.length > 0 && (k.titles = v), C.length > 0 && (k.itemTitles = C), A.length > 0 && (k.categoryMoves = A), b.length > 0 && (k.itemMoves = b), O.length > 0 && (k.hiddenCategories = O), N.length > 0 && (k.hiddenItems = N), 1 === Object.keys(k).length ? "" : JSON.stringify(k); +} +function buildMenuPresentation(e, t) { + let n = menu_layout_r(e), l = resolveMenuLayout(e, parseMenuLayout(t)), o = new Map(n.items.map((e)=>[ + e.path, + e + ])); + for (let [e, t] of l.itemTitles){ + let n = o.get(e); + n && o.set(e, { + ...n, + title: t + }); + } + let u = new Map(n.primaries.map((e)=>[ + primaryCategoryId(e.path), + e + ])), a = new Set(l.hiddenItemPaths); + for (let e of l.hiddenCategoryIds){ + let t = menu_layout_i(e); + t && a.add(t); + let n = l.categories.find((t)=>t.id === e); + if (n) for (let e of n.items)a.add(e); + } + return { + categories: l.categories.map((e)=>({ + id: e.id, + title: e.title, + primary: u.get(e.id) ?? null, + items: e.items.flatMap((e)=>{ + let t = o.get(e); + return t ? [ + t + ] : []; + }) + })), + hiddenCategoryIds: l.hiddenCategoryIds, + hiddenPaths: a, + configured: l.configured + }; +} + +;// CONCATENATED MODULE: ./web/resources/utils/menu-cache.ts +let menu_cache_e = "fluent_menu_cache"; +function menu_cache_t(e) { + let t = document.body?.getAttribute("data-menu-cache-scope"); + return t ? `${e}_${t}` : null; +} +function getCachedMenu() { + try { + let n = menu_cache_t(menu_cache_e); + if (!n) return null; + let r = sessionStorage.getItem(n); + if (!r) return null; + let u = JSON.parse(r); + if (u && "object" == typeof u) return { + tree: u, + raw: r + }; + } catch (e) {} + return null; +} +function saveMenuCache(n) { + try { + let r = menu_cache_t(menu_cache_e); + if (!r) return null; + let u = JSON.stringify(n); + return sessionStorage.setItem(r, u), u; + } catch (e) { + return null; + } +} +function saveRenderedHtmlCache() { + try { + let e = document.querySelector("#mainmenu"), n = menu_cache_t("fluent_sidebar_html"); + e && n && sessionStorage.setItem(n, e.innerHTML); + let r = document.querySelector("#tabmenu"), u = menu_cache_t("fluent_tabmenu_html"), l = Array.isArray(L?.env?.dispatchpath) ? JSON.stringify(L.env.dispatchpath) : ""; + r && r.children.length > 0 && u && l && sessionStorage.setItem(`${u}_${l}`, r.innerHTML); + } catch (e) {} +} +function getResolvedMenuLayout() { + let e = document.body?.getAttribute?.("data-menu-layout"); + if (e) try { + let t = JSON.parse(e); + if ("string" == typeof t || Array.isArray(t) || null === t) return t; + } catch (e) {} +} + +;// CONCATENATED MODULE: ./web/resources/utils/table-wrapper.ts +let table_wrapper_e = "table.table, table.cbi-section-table, div.table, div.cbi-section-table"; +function table_wrapper_t(e) { + let t = e.parentElement; + if (!t || t.classList.contains("table-wrapper")) return; + let n = document.createElement("div"); + n.className = "table-wrapper", t.insertBefore(n, e), n.append(e); +} +function table_wrapper_n(n) { + n instanceof HTMLElement && n.matches(table_wrapper_e) && table_wrapper_t(n), n.querySelectorAll(table_wrapper_e).forEach((e)=>{ + table_wrapper_t(e); + }); +} +function setupTableWrappers() { + document.body && (table_wrapper_n(document), new MutationObserver((e)=>{ + e.forEach((e)=>{ + e.addedNodes.forEach((e)=>{ + e instanceof Element && table_wrapper_n(e); + }); + }); + }).observe(document.body, { + childList: !0, + subtree: !0 + })); +} + +;// CONCATENATED MODULE: ./web/resources/utils/theme-features-lite.ts +function setupThemeFeatures() { + let e = document.body; + if (!e || e.dataset.fluentLiteFeaturesInitialized) return; + e.dataset.fluentLiteFeaturesInitialized = "true"; + let t = "1" === e.getAttribute("data-prefers-reduced-motion"), a = (a)=>e.setAttribute("data-reduce-motion", t && a ? "true" : "false"), d = window.matchMedia("(prefers-reduced-motion: reduce)"); + a(d.matches), t && d.addEventListener("change", (e)=>a(e.matches)); + let r = document.getElementById("theme-toggle"); + if (!r) return; + let i = (e)=>{ + let t = "auto" === e ? window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : e; + document.documentElement.setAttribute("data-theme", t), r.setAttribute("data-active-theme", t), r.setAttribute("data-mode", e); + }, o = L.rpc.declare({ + object: "luci.fluent", + method: "set_mode", + params: [ + "mode" + ] + }); + r.hidden = !1, i(e.getAttribute("data-theme-mode") || "auto"), requestAnimationFrame(()=>r.classList.add("visible")), r.addEventListener("click", async ()=>{ + if (r.disabled) return; + let t = e.getAttribute("data-theme-mode") || "auto", a = "dark" === t ? "light" : "light" === t ? "auto" : "dark"; + r.disabled = !0, i(a); + try { + let t = await o(a); + if (t?.result !== 0) throw Error(_("Failed to save theme mode.")); + e.setAttribute("data-theme-mode", a); + } catch (e) { + i(t), L.ui.addNotification(null, String(e), "error"); + } finally{ + r.disabled = !1; + } + }), window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", ()=>{ + let t = e.getAttribute("data-theme-mode") || "auto"; + "auto" === t && i(t); + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/menu-features-lite.ts + + +const setupMenuStartup = ()=>{}; +const setupMenuFeatures = (r)=>{ + setupTableWrappers(), setupThemeFeatures(); +}; + +;// CONCATENATED MODULE: ./web/resources/utils/sidebar-controller.ts +function applyDesktopSidebarState(e) { + document.body.setAttribute("data-sidebar-state", e), document.dispatchEvent(new CustomEvent("fluent-sidebar-state-change")); +} +function getDesktopSidebarState() { + try { + let e = localStorage.getItem("fluent-sidebar-state"); + return "collapsed" === e || "expanded" === e ? e : "expanded"; + } catch (e) { + return "expanded"; + } +} +function closeCollapsedPopups() { + document.querySelectorAll("#mainmenu ul.nav > li > a.menu.popup-open").forEach((e)=>{ + e.classList.remove("popup-open"); + }), document.querySelectorAll("#mainmenu ul.nav > li > ul.slide-menu.popup-open").forEach((e)=>{ + e.classList.remove("popup-open"), e.style.display = "none", e.style.top = ""; + }); +} +function adjustBrandTextSize() { + let e = document.querySelector(".sidenav-header .brand-text"); + if (e) { + let t = e.parentElement; + if (t) { + let a = t.clientWidth - 32; + if (a > 0) { + let t = 16; + for(e.style.fontSize = `${t}px`; e.scrollWidth > a && t > 9;)t -= 0.5, e.style.fontSize = `${t}px`; + } + } + } +} +function handleSidebarToggle(e) { + let t = document.querySelectorAll("a.showSide"), a = document.querySelector("#mainmenu"), d = document.querySelector(".darkMask"), o = document.querySelector(".main-right"); + 0 !== t.length && a && d && o && (Array.from(t).some((e)=>e.classList.contains("active")) ? (t.forEach((e)=>{ + e.classList.remove("active"); + }), a.classList.remove("active"), o.classList.remove("active"), d.classList.remove("active")) : (t.forEach((e)=>{ + e.classList.add("active"); + }), a.classList.add("active"), o.classList.add("active"), d.classList.add("active"), adjustBrandTextSize())); +} +function handleDesktopSidebarToggle(e) { + if (e.preventDefault(), e.stopPropagation(), window.innerWidth <= 768) return; + let t = "collapsed" == ("collapsed" === document.body.getAttribute("data-sidebar-state") ? "collapsed" : "expanded") ? "expanded" : "collapsed"; + closeCollapsedPopups(); + try { + localStorage.setItem("fluent-sidebar-state", t); + } catch (e) {} + applyDesktopSidebarState(t), "expanded" === t && adjustBrandTextSize(); +} +let sidebar_controller_e = !1; +function initSidebarController(t) { + if (sidebar_controller_e) return; + sidebar_controller_e = !0; + let a = document.querySelectorAll("a.showSide"), d = document.querySelector(".darkMask"), o = document.querySelector(".sidebar-collapse-toggle"), n = ui.createHandlerFn(t, "handleSidebarToggle") ?? (()=>{ + handleSidebarToggle(); + }), r = ui.createHandlerFn(t, "handleDesktopSidebarToggle") ?? ((e)=>{ + handleDesktopSidebarToggle(e); + }); + a.forEach((e)=>{ + e.addEventListener("click", n); + }), d && d.addEventListener("click", n), o && o.addEventListener("click", r), window.innerWidth > 768 ? applyDesktopSidebarState(getDesktopSidebarState()) : document.body.setAttribute("data-sidebar-state", "expanded"), window.addEventListener("resize", ()=>{ + adjustBrandTextSize(), window.innerWidth > 768 ? applyDesktopSidebarState(getDesktopSidebarState()) : document.body.setAttribute("data-sidebar-state", "expanded"); + }), document.addEventListener("click", (e)=>{ + if (window.innerWidth <= 768 || "collapsed" !== document.body.getAttribute("data-sidebar-state")) return; + let t = e.target, a = document.querySelector("#mainmenu"); + t && a?.contains(t) || closeCollapsedPopups(); + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/slide-animations.ts +let slide_animations_t = { + fast: 200, + normal: 400, + slow: 600 +}, slide_animations_e = (e)=>{ + if ("number" == typeof e) return e || slide_animations_t.normal; + switch(e){ + case "fast": + case "slow": + return slide_animations_t[e]; + default: + return slide_animations_t.normal; + } +}; +const SlideAnimations = { + durations: slide_animations_t, + runningAnimations: new WeakMap(), + slideDown (t, i, n) { + if (!t) return void console.warn("SlideAnimations.slideDown: No element provided"); + this.stop(t); + let s = slide_animations_e(i), o = { + display: t.style.display, + overflow: t.style.overflow, + height: t.style.height, + transition: t.style.transition + }; + t.style.display = "block", t.style.overflow = "hidden", t.style.height = "0px", t.style.transition = `height ${s}ms ease-out`, t.offsetHeight; + let l = t.scrollHeight; + t.style.height = `${l}px`; + let r = ()=>{ + if (t.style.height = o.height || "", t.style.overflow = o.overflow || "", t.style.transition = o.transition || "", this.runningAnimations.delete(t), n && "function" == typeof n) try { + n.call(t); + } catch (t) { + console.error("SlideAnimations callback error:", t); + } + }, a = setTimeout(r, s); + this.runningAnimations.set(t, { + timeoutId: a, + cleanup: r + }); + }, + slideUp (t, i, n) { + if (!t) return void console.warn("SlideAnimations.slideUp: No element provided"); + this.stop(t); + let s = slide_animations_e(i), o = { + display: t.style.display, + overflow: t.style.overflow, + height: t.style.height, + transition: t.style.transition + }, l = t.scrollHeight; + t.style.overflow = "hidden", t.style.height = `${l}px`, t.style.transition = `height ${s}ms ease-out`, t.offsetHeight, t.style.height = "0px"; + let r = ()=>{ + if (t.style.display = "none", t.style.height = o.height || "", t.style.overflow = o.overflow || "", t.style.transition = o.transition || "", this.runningAnimations.delete(t), n && "function" == typeof n) try { + n.call(t); + } catch (t) { + console.error("SlideAnimations callback error:", t); + } + }, a = setTimeout(r, s); + this.runningAnimations.set(t, { + timeoutId: a, + cleanup: r + }); + }, + stop (t) { + if (!t) return; + let e = this.runningAnimations.get(t); + e && (clearTimeout(e.timeoutId), e.cleanup()), t.style.transition = "", t.offsetHeight; + }, + isAnimating (t) { + return this.runningAnimations.has(t); + } +}; + +;// CONCATENATED MODULE: ./web/resources/menu-fluent.tsx + + + + + + +let menu_fluent_v = !1; +function menu_fluent_y(e) { + menu_fluent_v || (menu_fluent_v = !0, setupMenuFeatures()), setupMenuFeatures(e); +} +const main = baseclass.extend({ + async __init__ () { + setupMenuStartup(), initSidebarController(this); + let e = getResolvedMenuLayout(), t = getCachedMenu(), l = !1; + if (t) { + let i = buildMenuPresentation(t.tree, e ?? null); + this.render(t.tree, i), menu_fluent_y(i), l = !0; + } + try { + let i = void 0 === e, [a] = await Promise.all([ + ui.menu.load(), + i ? L.uci.load("fluent") : Promise.resolve() + ]), s = saveMenuCache(a); + if (!l || s !== t?.raw) { + if (i) { + let t = L.uci.get_first("fluent", "global", "menu_layout"); + e = "string" == typeof t || Array.isArray(t) ? t : null; + } + let t = buildMenuPresentation(a, e ?? null); + this.render(a, t), menu_fluent_y(t); + } + } catch (e) { + l || console.error("Fluent menu: Failed to load menu data", e); + } + }, + render (e, t) { + let l = e, n = ""; + if (t.configured) this.renderConfiguredMenu(t); + else { + let t = ui.menu.getChildren(e); + for(let e = 0; e < t.length; e++)(L.env.requestpath.length ? t[e].name === L.env.requestpath[0] : 0 === e) && this.renderMainMenu(t[e], t[e].name); + } + if (L.env.dispatchpath.length >= 3) { + for(let e = 0; e < 3 && l; e++){ + let t = L.env.dispatchpath[e]; + l = l.children?.[t], n = n + (n ? "/" : "") + t; + } + l && this.renderTabMenu(l, n, void 0, t.hiddenPaths); + } else { + let e = document.querySelector("#tabmenu"); + e && (e.innerHTML = "", e.style.display = "none"); + } + saveRenderedHtmlCache(); + }, + handleMenuExpand (e) { + let t = e.currentTarget; + if (!t) return; + let l = t.parentNode, n = t.nextElementSibling, i = window.innerWidth > 768 && "collapsed" === document.body.getAttribute("data-sidebar-state"), a = !1; + if (document.querySelectorAll(i ? ".main .main-left .nav > li > ul.slide-menu.popup-open" : ".main .main-left .nav > li > ul.slide-menu.active").forEach((e)=>{ + a || e !== n || (a = !0), e.classList.remove("popup-open", "active"), e.previousElementSibling?.classList.remove("popup-open", "active"), SlideAnimations.stop(e), i ? (e.style.display = "none", e.style.top = "") : SlideAnimations.slideUp(e, "fast"); + }), n) { + if (!a) { + let e = l?.querySelector(".slide-menu"); + if (e) { + if (n.classList.add(i ? "popup-open" : "active"), t.classList.add(i ? "popup-open" : "active"), i) { + SlideAnimations.stop(e), e.style.display = "block"; + let l = t.getBoundingClientRect(), n = e.offsetHeight, i = Math.max(8, window.innerHeight - n - 8), a = l.top - 8; + e.style.top = `${Math.min(i, Math.max(8, a))}px`; + } else e.style.top = "", SlideAnimations.slideDown(e, "fast"); + e.querySelectorAll("li > a").forEach((e)=>{ + e.addEventListener("click", ()=>{ + closeCollapsedPopups(); + }, { + once: !0 + }); + }); + } + t.blur(); + } + document.dispatchEvent(new CustomEvent("fluent-menu-expand")), e.preventDefault(), e.stopPropagation(); + } + }, + renderMainMenu (n, i, a) { + let r = (a || 0) + 1, s = a && n.title ? n.title.replace(/ /g, "_") : void 0, d = jsx("ul", { + class: a ? "slide-menu" : "nav", + "data-parent": s || void 0 + }), u = ui.menu.getChildren(n); + if (0 === u.length || r > 2) return jsx(Fragment, {}); + for(let l = 0; l < u.length; l++){ + let a = u[l], s = L.env.dispatchpath[r] === a.name && L.env.dispatchpath[r - 1] === n.name, c = this.renderMainMenu(a, `${i}/${a.name}`, r), o = c.children.length > 0, p = o ? "slide" : null, h = o ? "menu" : "item"; + s && (d.classList.add("active"), p = p ? `${p} active` : "null active"); + let m = s ? `${h} active` : h, f = jsxs("li", { + class: p ?? void 0, + children: [ + jsxs("a", { + href: L.url(i, a.name), + onclick: 1 === r ? ui.createHandlerFn(this, "handleMenuExpand") : null, + class: m, + "data-title": (a.title || "").replace(/ /g, "_"), + children: [ + 1 === r || 2 === r ? jsx("span", { + class: "menu-icon" + }) : null, + jsx("span", { + class: "menu-label", + children: _(a.title || "") + }) + ] + }), + c + ] + }); + d.appendChild(f); + } + if (1 === r) { + let e = document.querySelector("#mainmenu"); + if (e) { + let t = "function" == typeof e.querySelector ? e.querySelector("ul.nav") : null; + t && "function" == typeof t.replaceWith ? t.replaceWith(d) : e.appendChild(d), e.style.display = "", adjustBrandTextSize(); + } + } + return d; + }, + renderConfiguredMenu (l) { + let n = jsx("ul", { + class: "nav" + }); + for (let i of l.categories){ + if (l.hiddenCategoryIds.has(i.id)) continue; + let a = i.items.filter((e)=>!l.hiddenPaths.has(e.path)); + if (!i.primary && 0 === a.length) continue; + let r = jsx("ul", { + class: "slide-menu", + "data-parent": i.title.replace(/ /g, "_") + }), s = !1; + for (let l of a){ + let n = l.pathSegments.every((e, t)=>L.env.dispatchpath[t] === e); + s ||= n, r.appendChild(jsx("li", { + class: n ? "active" : void 0, + children: jsxs("a", { + href: L.url(l.path), + class: `item${n ? " active" : ""}`, + "data-title": l.rawTitle.replace(/ /g, "_"), + children: [ + jsx("span", { + class: "menu-icon" + }), + jsx("span", { + class: "menu-label", + children: l.title + }) + ] + }) + })); + } + s && r.classList.add("active"), i.primary && L.env.dispatchpath.length <= 2 && (s ||= i.primary.pathSegments.every((e, t)=>L.env.dispatchpath[t] === e)); + let d = a.length > 0, u = i.primary?.path ?? a[0]?.path ?? "#", c = i.primary?.rawTitle ?? i.title, o = `${d ? "slide" : ""}${s ? " active" : ""}`.trim() || void 0; + n.appendChild(jsxs("li", { + class: o, + children: [ + jsxs("a", { + href: "#" === u ? u : L.url(u), + onclick: d ? ui.createHandlerFn(this, "handleMenuExpand") : null, + class: `${d ? "menu" : "item"}${s ? " active" : ""}`, + "data-title": c.replace(/ /g, "_"), + children: [ + jsx("span", { + class: "menu-icon" + }), + jsx("span", { + class: "menu-label", + children: i.title + }) + ] + }), + d ? r : null + ] + })); + } + let i = document.querySelector("#mainmenu"); + if (i) { + let e = "function" == typeof i.querySelector ? i.querySelector("ul.nav") : null; + e && "function" == typeof e.replaceWith ? e.replaceWith(n) : i.appendChild(n), i.style.display = "", adjustBrandTextSize(); + } + return n; + }, + renderTabMenu (t, n, i, a) { + let r = L.env.dispatchpath.slice(0, 2).join("/"), s = L.env.dispatchpath.slice(0, 3).join("/"); + if (a.has(r) || a.has(s)) return jsx(Fragment, {}); + let d = document.querySelector("#tabmenu"), u = (i || 0) + 1, c = jsx("ul", { + class: "tabs" + }), o = ui.menu.getChildren(t), p = null; + if (0 === o.length) return d && 1 === u && (d.innerHTML = "", d.style.display = "none"), jsx(Fragment, {}); + for(let t = 0; t < o.length; t++){ + let l = o[t], i = L.env.dispatchpath[u + 2] === l.name, a = i ? " active" : "", r = jsx("li", { + class: `tabmenu-item-${l.name}${a}`, + children: jsx("a", { + href: L.url(n, l.name), + children: _(l.title || "") + }) + }); + c.appendChild(r), i && (p = l); + } + if (d && (1 === u && (d.innerHTML = ""), d.appendChild(c), d.style.display = "", p)) { + let e = this.renderTabMenu(p, `${n}/${p.name}`, u, a); + e.children.length > 0 && d.appendChild(e); + } + return c; + }, + adjustBrandTextSize () { + adjustBrandTextSize(); + }, + handleSidebarToggle (e) { + handleSidebarToggle(e); + }, + handleDesktopSidebarToggle (e) { + handleDesktopSidebarToggle(e); + } +}); + + +return main; \ No newline at end of file diff --git a/luci-theme-fluent-lite/htdocs/luci-static/resources/view/fluent-config.js b/luci-theme-fluent-lite/htdocs/luci-static/resources/view/fluent-config.js new file mode 100644 index 000000000..9475ff7d3 --- /dev/null +++ b/luci-theme-fluent-lite/htdocs/luci-static/resources/view/fluent-config.js @@ -0,0 +1,563 @@ +"use strict"; +"require baseclass"; +"require ui"; +"require dom"; +// WARN: generated by rsbuild - do not edit + +// The require scope +var __webpack_require__ = {}; + +// webpack/runtime/define_property_getters +(() => { +__webpack_require__.d = (exports, getters, values) => { + var define = (defs, kind) => { + for(var key in defs) { + if(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] }); + } + } + }; + define(getters, "get"); + define(values, "value"); +}; +})(); +// webpack/runtime/has_own_property +(() => { +__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +})(); +// webpack/runtime/make_namespace_object +(() => { +// define __esModule on exports +__webpack_require__.r = (exports) => { + if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { + Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + } + Object.defineProperty(exports, '__esModule', { value: true }); +}; +})(); +var __webpack_exports__ = {}; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + main: () => (/* binding */ main) +}); + +;// CONCATENATED MODULE: ./web/resources/fluent-defaults.ts +const FLUENT_DEFAULTS = { + mode: "auto", + primary: "#0078d4", + dark_primary: "#4da6ff", + progressbar_font: "#2e2b60", + dark_progressbar_font: "#d6d9e5", + page_bg: "#fafafa", + card_bg: "#ffffff", + sidebar_bg: "#f3f3f3", + dark_page_bg: "#1b1b1b", + dark_card_bg: "#2d2d2d", + dark_sidebar_bg: "#1f1f1f", + font_weight: "normal", + font_size: "14", + border_radius: "4", + control_height: "32", + sidebar_width: "260", + header_height: "48", + login_bg: "microsoft", + blur: "40", + transparency: "0.8", + blur_dark: "20", + transparency_dark: "0.8", + card_shadow: "small", + transition_speed: "normal", + custom_css: "", + custom_select: "1", + view_transition: "1", + tab_animation: "1", + prefers_reduced_motion: "1", + loading_bar: "1", + sticky_action_bar: "1", + uci_changes_preview: "1", + direction_mode: "auto", + progressbar_text_position: "top-start" +}; +const fluentFlagDefault = (r)=>"1" === r; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/shared.ts +let shared_e = L.form; +const omitDefaultValue = (e)=>{ + let t = e.parse.bind(e); + e.parse = (r)=>e.isActive(r) && e.formvalue(r) === e.default ? e.isValid(r) ? (e.remove(r), Promise.resolve()) : t(r) : t(r); +}; +const transparencySteps = [ + 0, + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 +]; +let shared_t = "fluent-live-preview", shared_r = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i, shared_a = { + primary: { + cssVar: "--fluent-primary", + isDark: !1 + }, + dark_primary: { + cssVar: "--fluent-primary", + isDark: !0 + }, + page_bg: { + cssVar: "--fluent-bg", + isDark: !1 + }, + dark_page_bg: { + cssVar: "--fluent-bg", + isDark: !0 + }, + card_bg: { + cssVar: "--fluent-bg-card", + isDark: !1 + }, + dark_card_bg: { + cssVar: "--fluent-bg-card", + isDark: !0 + }, + sidebar_bg: { + cssVar: "--fluent-sidebar-bg", + isDark: !1 + }, + dark_sidebar_bg: { + cssVar: "--fluent-sidebar-bg", + isDark: !0 + }, + progressbar_font: { + cssVar: "--fluent-progressbar-font-color", + isDark: !1 + }, + dark_progressbar_font: { + cssVar: "--fluent-progressbar-font-color", + isDark: !0 + } +}, shared_l = new globalThis.Map(); +const configureHexColorValue = (s, o, n = !1)=>{ + s.validate = (e, t)=>!e || shared_r.test(String(t)) || _("Expecting: %s").format(_("valid HEX color value")), s.render = (i, c, u)=>{ + let d = shared_e.Value.prototype.render.call(s, i, c, u), p = ()=>{ + let e = document.querySelector(`[id^="widget.cbid.fluent."][id$=".${o}"]`); + e && ((e, t)=>{ + if ("true" === e.dataset.fluentColorPicker) return; + let a = e.parentElement; + if (!a) return; + e.dataset.fluentColorPicker = "true", e.classList.add("fluent-color-field__text"); + let l = document.createElement("div"); + l.className = "fluent-color-field"; + let s = document.createElement("label"); + s.className = "fluent-color-swatch", s.title = _("Choose color"); + let o = document.createElement("input"); + o.type = "color", o.className = "fluent-color-swatch__input", o.setAttribute("aria-label", _("Choose color")); + let n = document.createElement("span"); + n.className = "fluent-color-swatch__preview"; + let i = (e)=>{ + shared_r.test(e) && (o.value = e, n.style.backgroundColor = e); + }; + i(e.value), o.addEventListener("input", ()=>{ + e.value = o.value, n.style.backgroundColor = o.value, shared_r.test(o.value) && t(o.value); + }), e.addEventListener("input", ()=>{ + i(e.value), shared_r.test(e.value) && t(e.value); + }), s.appendChild(o), s.appendChild(n), a.insertBefore(l, e), l.appendChild(e), l.appendChild(s); + })(e, (e)=>((e, r)=>{ + let s = shared_a[e]; + if (!s) return; + let o = `${s.isDark ? "dark" : "light"}|${s.cssVar}`; + shared_l.set(o, { + selector: s.isDark ? ':root[data-theme="dark"]' : ":root", + cssVar: s.cssVar, + value: r + }), (()=>{ + let e, r = ((e = document.getElementById(shared_t)) || ((e = document.createElement("style")).id = shared_t, document.head.appendChild(e)), e), a = new globalThis.Map(); + for (let e of shared_l.values()){ + let t = a.get(e.selector) ?? []; + t.push(`${e.cssVar}: ${e.value};`), a.set(e.selector, t); + } + let s = ""; + for (let [e, t] of a)s += `${e} { ${t.join(" ")} }\n`; + r.textContent = s; + })(); + })(o, e)); + }; + return n ? requestAnimationFrame(p) : setTimeout(p, 0), d; + }; +}; +const createModeSubtabs = (t, r, a)=>{ + let l = t.taboption(r, shared_e.SectionValue, a, shared_e.TypedSection, "global").subsection; + return l.anonymous = !0, l.addremove = !1, l.tab("light", _("Light mode")), l.tab("dark", _("Dark mode")), l; +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/advanced.ts +let advanced_e = L.form; + + +const registerAdvancedTab = (d)=>{ + d.tab("advanced", _("Advanced"), _("Adjust layout, typography, transition timing, shadows, and inject custom CSS variables or rules when the built-in controls are not enough.")); + { + let o = d.taboption("advanced", advanced_e.Value, "font_size", _("Base font size"), _("Sets the base interface font size in pixels. Most theme text scales from this value through the Fluent CSS variables. Recommended range: 12-18px.")); + o.datatype = "range(12,18)", o.default = FLUENT_DEFAULTS.font_size, omitDefaultValue(o), o.placeholder = FLUENT_DEFAULTS.font_size; + } + { + let o = d.taboption("advanced", advanced_e.Value, "sidebar_width", _("Sidebar width"), _("Width of the main navigation sidebar in pixels.")); + o.datatype = "range(200,420)", o.default = FLUENT_DEFAULTS.sidebar_width, omitDefaultValue(o), o.placeholder = FLUENT_DEFAULTS.sidebar_width; + } + { + let o = d.taboption("advanced", advanced_e.Value, "header_height", _("Header height"), _("Height of the top header bar in pixels.")); + o.datatype = "range(40,96)", o.default = FLUENT_DEFAULTS.header_height, omitDefaultValue(o), o.placeholder = FLUENT_DEFAULTS.header_height; + } + { + let o = d.taboption("advanced", advanced_e.ListValue, "border_radius", _("Corner radius"), _("Controls the shared Fluent corner radius tokens used by cards, buttons, inputs, and related UI surfaces.")); + o.value("0", _("Square (0px)")), o.value("2", _("Small (2px)")), o.value("4", _("Medium (4px)")), o.value("6", _("Rounded (6px)")), o.value("8", _("Large (8px)")), o.value("12", _("Extra large (12px)")), o.default = FLUENT_DEFAULTS.border_radius, omitDefaultValue(o); + } + { + let o = d.taboption("advanced", advanced_e.ListValue, "card_shadow", _("Card shadow"), _("Select the shadow depth applied to themed cards and panels.")); + o.value("none", _("None")), o.value("small", _("Small")), o.value("medium", _("Medium")), o.value("large", _("Large")), o.default = FLUENT_DEFAULTS.card_shadow, omitDefaultValue(o); + } + { + let o = d.taboption("advanced", advanced_e.ListValue, "transition_speed", _("Theme transition speed"), _("Controls the shared Fluent transition timing used by menu, header, and other theme animations.")); + o.value("fast", _("Fast")), o.value("normal", _("Normal")), o.value("slow", _("Slow")), o.value("none", _("Disabled")), o.default = FLUENT_DEFAULTS.transition_speed, omitDefaultValue(o); + } + { + let o = d.taboption("advanced", advanced_e.TextValue, "custom_css", _("Custom CSS"), _("Optional raw CSS injected into the Fluent header template. Use this for extra --fluent-* variable overrides or page-specific tweaks that are not exposed as dedicated options.")); + o.default = FLUENT_DEFAULTS.custom_css, omitDefaultValue(o), o.rmempty = !0, o.rows = 12, o.wrap = "off", o.placeholder = ":root {\n --fluent-sidebar-width: 280px;\n --fluent-card-shadow: none;\n}"; + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/colors.ts +let colors_r = L.form; + + +const registerColorsTab = (d)=>{ + d.tab("colors", _("Colors")); + let l = createModeSubtabs(d, "colors", "colors_mode_tabs"); + { + let o = l.taboption("light", colors_r.Value, "primary", _("Accent color"), _("HEX color used as the primary Fluent accent when the interface is rendered in light mode.")); + o.default = FLUENT_DEFAULTS.primary, omitDefaultValue(o), configureHexColorValue(o, "primary"); + } + { + let o = l.taboption("light", colors_r.Value, "progressbar_font", _("Progress bar text color"), _("HEX color used for progress-bar labels while the interface is rendered in light mode.")); + o.default = FLUENT_DEFAULTS.progressbar_font, omitDefaultValue(o), configureHexColorValue(o, "progressbar_font"); + } + { + let o = l.taboption("light", colors_r.Value, "page_bg", _("Page background"), _("HEX color used for the main page background in light mode.")); + o.default = FLUENT_DEFAULTS.page_bg, omitDefaultValue(o), configureHexColorValue(o, "page_bg"); + } + { + let o = l.taboption("light", colors_r.Value, "card_bg", _("Card background"), _("HEX color used for container/card elements in light mode.")); + o.default = FLUENT_DEFAULTS.card_bg, omitDefaultValue(o), configureHexColorValue(o, "card_bg"); + } + { + let o = l.taboption("light", colors_r.Value, "sidebar_bg", _("Sidebar background"), _("HEX color used for the navigation sidebar in light mode.")); + o.default = FLUENT_DEFAULTS.sidebar_bg, omitDefaultValue(o), configureHexColorValue(o, "sidebar_bg"); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_primary", _("Accent color"), _("HEX color used as the primary Fluent accent when the interface is rendered in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_primary, omitDefaultValue(o), configureHexColorValue(o, "dark_primary", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_progressbar_font", _("Progress bar text color"), _("HEX color used for progress-bar labels while the interface is rendered in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_progressbar_font, omitDefaultValue(o), configureHexColorValue(o, "dark_progressbar_font", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_page_bg", _("Page background"), _("HEX color used for the main page background in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_page_bg, omitDefaultValue(o), configureHexColorValue(o, "dark_page_bg", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_card_bg", _("Card background"), _("HEX color used for container/card elements in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_card_bg, omitDefaultValue(o), configureHexColorValue(o, "dark_card_bg", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_sidebar_bg", _("Sidebar background"), _("HEX color used for the navigation sidebar in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_sidebar_bg, omitDefaultValue(o), configureHexColorValue(o, "dark_sidebar_bg", !0); + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/general.ts +let general_e = L.form; + + +const registerGeneralTab = (l, r = !0)=>{ + l.tab("general", _("General")); + { + let o = l.taboption("general", general_e.ListValue, "mode", _("Color mode")); + o.value("auto", _("Follow system")), o.value("light", _("Force light mode")), o.value("dark", _("Force dark mode")), o.default = FLUENT_DEFAULTS.mode, omitDefaultValue(o), o.description = _("Use the system/browser preference, or always render the Fluent theme in a fixed light or dark palette."); + } + { + let o = l.taboption("general", general_e.ListValue, "direction_mode", _("Text direction")); + o.value("auto", _("Automatic (Arabic/Persian locales only)")), o.value("rtl", _("Force RTL")), o.value("ltr", _("Force LTR")), o.default = FLUENT_DEFAULTS.direction_mode, omitDefaultValue(o), o.description = _("Choose the document direction for authenticated and login pages. Automatic mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, fa-*) to RTL and falls back to LTR for missing or unrecognized locales."); + } + { + let o = l.taboption("general", general_e.ListValue, "font_weight", _("Navigation font weight")); + o.value("normal", _("Normal")), o.value("600", _("Semibold")), o.default = FLUENT_DEFAULTS.font_weight, omitDefaultValue(o), o.description = _("Controls the font weight used by main navigation labels and related theme text accents."); + } + { + let o = l.taboption("general", general_e.ListValue, "control_height", _("Control height")); + o.value("32", _("Compact (32px)")), o.value("42", _("Comfortable (42px)")), o.default = FLUENT_DEFAULTS.control_height, omitDefaultValue(o), o.description = _("Applies to standard buttons, inputs, selects, and similar form controls across the theme."); + } + if (r) { + let r = l.taboption("general", general_e.Flag, "custom_select", _("Use Fluent custom select dropdowns"), _("Replace native select elements with the theme's custom dropdown widget.")); + r.default = fluentFlagDefault(FLUENT_DEFAULTS.custom_select) ? r.enabled : r.disabled, omitDefaultValue(r); + } + { + let r = l.taboption("general", general_e.Flag, "sticky_action_bar", _("Keep action bar visible while scrolling"), _("Keep Save, Apply, and Reset actions fixed at the bottom of the viewport while scrolling.")); + r.default = fluentFlagDefault(FLUENT_DEFAULTS.sticky_action_bar) ? r.enabled : r.disabled, omitDefaultValue(r); + } + { + let o = l.taboption("general", general_e.ListValue, "progressbar_text_position", _("Progress bar text position")); + o.value("top-start", _("Above bar, start")), o.value("bottom-start", _("Below bar, start")), o.value("top-end", _("Above bar, end")), o.value("bottom-end", _("Below bar, end")), o.default = FLUENT_DEFAULTS.progressbar_text_position, omitDefaultValue(o), o.description = _("Position of progress-bar labels relative to the bar. Start/end alignment follows the text direction (LTR or RTL)."); + } +}; + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-factory.ts +const Fragment = Symbol.for("jsx.fragment"); +function jsx_factory_e(e, t) { + let { children: n, ...r } = null === t || "object" != typeof t || Array.isArray(t) ? {} : t, o = function e(t, n = []) { + for (let r of t)null != r && "boolean" != typeof r && (Array.isArray(r) ? e(r, n) : n.push(r instanceof Node ? r : String(r))); + return n; + }(null == n ? [] : Array.isArray(n) ? n : [ + n + ]); + if (e === Fragment) { + let e = document.createDocumentFragment(); + return e.append(...o), e; + } + if ("function" == typeof e) { + let t = Reflect.apply(e, void 0, [ + { + ...r, + children: o + } + ]); + if (!(t instanceof Node)) throw TypeError("JSX components must return a DOM Node"); + return t; + } + if ("string" != typeof e) throw TypeError("JSX element types must be tag names or component functions"); + let f = {}, l = { + ...r + }; + for (let [e, t] of Object.entries(l))e.startsWith("on") && "function" == typeof t ? (f[e] = t, delete l[e]) : "boolean" == typeof t && (t ? l[e] = e : delete l[e]); + let s = Object.keys(l).length > 0 ? o.length > 1 ? E(e, l, o) : E(e, l, o[0]) : o.length > 1 ? E(e, {}, o) : E(e, {}, o[0]); + for (let [e, t] of Object.entries(f)){ + let n = e.slice(2).toLowerCase(); + s.addEventListener(n, t); + } + return s; +} +function jsx(t, n) { + return jsx_factory_e(t, n); +} +function jsxs(t, n) { + return jsx_factory_e(t, n); +} +function jsxDEV(t, n) { + return jsx_factory_e(t, n); +} + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-runtime.ts + + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/login.tsx + +let login_a = L.form, login_n = L.rpc, login_l = L.fs, login_i = L.ui, login_o = login_n.declare({ + object: "luci.fluent", + method: "avail", + expect: { + avail: 0 + } +}), login_r = login_n.declare({ + object: "luci.fluent", + method: "remove", + params: [ + "filename" + ], + expect: { + result: 0 + } +}), login_d = login_n.declare({ + object: "luci.fluent", + method: "rename", + params: [ + "newname" + ], + expect: { + result: 0 + } +}); + + +let login_p = new Set([ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "mp4", + "webm" +]), b = (e)=>e.split(".").pop()?.toLowerCase() ?? "", m = (e)=>login_p.has(b(e)), login_f = (e)=>e >= 1048576 ? `${(e / 1024 / 1024).toFixed(1)} GiB` : e >= 1024 ? `${(e / 1024).toFixed(1)} MiB` : `${e} KiB`, h = (e, t)=>{ + if (0 !== e) throw Error(`${t} failed with code ${e}.`); +}, v = (e, t)=>{ + dom.content(e, t); +}, login_k = login_a.DummyValue.extend({ + renderWidget: (a, n, c)=>{ + let s = jsx("div", { + class: "cbi-value-description fluent-bg-status", + children: "Ready to upload or remove custom backgrounds." + }), u = jsx("div", { + class: "fluent-bg-hint", + children: "Supported formats: JPG, PNG, GIF, WEBP, MP4, WEBM." + }), g = jsx("button", { + class: "btn cbi-button cbi-button-action", + type: "button", + children: "Upload background" + }), p = jsx("div", { + class: "fluent-bg-actions", + children: g + }), k = jsx("div", { + class: "fluent-bg-list" + }), y = (e)=>{ + v(s, [ + document.createTextNode(e) + ]); + }, $ = ()=>login_l.list("/www/luci-static/fluent/background").catch(()=>[]).then((a)=>{ + let n = a.filter((e)=>"file" === e.type && m(String(e.name ?? ""))).sort((e, t)=>String(e.name ?? "").localeCompare(String(t.name ?? ""))); + n.length ? v(k, n.map((a)=>{ + let n, l, o, d = String(a.name ?? ""), c = jsx("button", { + class: "btn cbi-button cbi-button-remove", + type: "button", + children: "Delete" + }), s = login_i.createHandlerFn(undefined, ()=>(y(`Deleting ${d}...`), login_r(d).then((e)=>(h(e, `Deleting ${d}`), $().then(()=>{ + y(`Deleted ${d}.`); + }))).catch((e)=>{ + y(`Failed to delete ${d}: ${e instanceof Error ? e.message : String(e)}`); + }))); + return s && c.addEventListener("click", s), jsxs("div", { + class: "fluent-bg-item", + children: [ + (n = b(d), l = `/luci-static/fluent/background/${encodeURIComponent(d)}`, "mp4" === n || "webm" === n ? jsx("video", { + class: "fluent-bg-preview fluent-bg-preview-video", + muted: !0, + playsInline: !0, + preload: "metadata", + src: l + }) : jsx("div", { + class: "fluent-bg-preview", + style: `background-image:url('${l.replace(/'/g, "%27")}')` + })), + jsxs("div", { + class: "fluent-bg-meta", + children: [ + jsx("strong", { + class: "fluent-bg-name", + title: d, + children: d + }), + jsx("span", { + class: "fluent-bg-size", + children: (o = Number(a.size ?? 0)) > 0 ? login_f(Math.max(1, Math.ceil(o / 1024))) : "Unknown size" + }) + ] + }), + jsx("div", { + class: "fluent-bg-item-actions", + children: c + }) + ] + }); + })) : v(k, jsxs("div", { + class: "fluent-bg-empty", + children: [ + jsx("strong", { + children: "No custom backgrounds yet" + }), + jsx("span", { + children: "Upload an image or video to use it on the login page." + }) + ] + })); + }); + return g.addEventListener("click", ()=>{ + y("Selecting background file..."), login_i.uploadFile("/tmp/fluent_background.tmp").then((e)=>{ + let t; + if (!e?.name) throw Error("Upload did not return a filename."); + let a = "" !== (t = e.name.replace(/^.*[\\/]/, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")) && "." !== t && ".." !== t && m(t) ? t : `background-${Date.now()}.jpg`; + return y(`Saving ${a}...`), login_d(a).then((e)=>(h(e, `Saving ${a}`), $().then(()=>{ + y(`Saved ${a}.`); + }))).catch((e)=>{ + y(`Failed to save ${a}: ${e instanceof Error ? e.message : String(e)}`); + }); + }).catch((e)=>{ + let t = e instanceof Error ? e.message : String(e); + t && "false" !== t ? y(`Upload failed: ${t}`) : y("Upload canceled."); + }); + }), login_o().then((e)=>{ + y(`Ready to upload or remove custom backgrounds. Available space: ${login_f(e)}.`); + }).catch(()=>{ + y("Ready to upload or remove custom backgrounds."); + }), $(), jsx("div", { + class: "fluent-bg-manager", + children: [ + s, + u, + p, + k + ] + }); + } +}); +const registerLoginTab = (e)=>{ + e.tab("login", _("Login page"), _("Customize the login page background, card opacity, and blur radius for light and dark modes.")); + { + let t = e.taboption("login", login_a.ListValue, "login_bg", "Background source", "Select the background image source for the login page."); + t.value("microsoft", "Microsoft dynamic canvas"), t.value("custom", "Custom background"), t.value("bing", "Bing daily wallpaper"), t.default = FLUENT_DEFAULTS.login_bg, omitDefaultValue(t); + } + e.taboption("login", login_k, "_bg_mgr", "Custom backgrounds", "Upload and manage custom background images for the login page.").depends("login_bg", "custom"); + let t = createModeSubtabs(e, "login", "login_mode_tabs"); + { + let e = t.taboption("light", login_a.ListValue, "transparency", _("Login card opacity"), _("Opacity of the login card in light mode. 0 is fully transparent and 1 is fully opaque.")); + for (let t of transparencySteps)e.value(String(t)); + e.default = FLUENT_DEFAULTS.transparency, omitDefaultValue(e); + } + { + let e = t.taboption("light", login_a.Value, "blur", _("Backdrop blur radius"), _("Blur radius in pixels behind the login card in light mode. Use 0 to disable blur.")); + e.datatype = "ufloat", e.default = FLUENT_DEFAULTS.blur, omitDefaultValue(e); + } + { + let e = t.taboption("dark", login_a.ListValue, "transparency_dark", _("Login card opacity"), _("Opacity of the login card in dark mode. 0 is fully transparent and 1 is fully opaque.")); + for (let t of transparencySteps)e.value(String(t)); + e.default = FLUENT_DEFAULTS.transparency_dark, omitDefaultValue(e); + } + { + let e = t.taboption("dark", login_a.Value, "blur_dark", _("Backdrop blur radius"), _("Blur radius in pixels behind the login card in dark mode. Use 0 to disable blur.")); + e.datatype = "ufloat", e.default = FLUENT_DEFAULTS.blur_dark, omitDefaultValue(e); + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config-lite.tsx +let fluent_config_lite_e = L.form, fluent_config_lite_n = L.uci; + + + + +class fluent_config_lite_l extends L.view { + load() { + return fluent_config_lite_n.load("fluent"); + } + render() { + let n = new fluent_config_lite_e.Map("fluent", _("Fluent theme settings"), _("Configure color mode, accent colors, layout sizing, login-page appearance, and advanced CSS overrides for luci-theme-fluent-lite.")), l = n.section(fluent_config_lite_e.TypedSection, "global", _("Theme settings")); + return l.addremove = !1, l.anonymous = !0, registerGeneralTab(l, !1), registerColorsTab(l), registerLoginTab(l), registerAdvancedTab(l), n.render(); + } +} +const main = fluent_config_lite_l; + + +return main; \ No newline at end of file diff --git a/luci-theme-fluent-lite/root/usr/libexec/rpcd/luci.fluent b/luci-theme-fluent-lite/root/usr/libexec/rpcd/luci.fluent new file mode 100755 index 000000000..99477c41c --- /dev/null +++ b/luci-theme-fluent-lite/root/usr/libexec/rpcd/luci.fluent @@ -0,0 +1,80 @@ +#!/bin/sh + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +readonly bg_path="/www/luci-static/fluent/background" +readonly tmp_path="/tmp/fluent_background.tmp" + +case "$1" in +"list") + json_init + json_add_object "avail" + json_close_object + json_add_object "remove" + json_add_string "filename" "filename" + json_close_object + json_add_object "rename" + json_add_string "newname" "filename" + json_close_object + json_add_object "set_mode" + json_add_string "mode" "mode" + json_close_object + json_dump + json_cleanup + ;; +"call") + case "$2" in + "avail") + json_init + json_add_int "avail" "$(df | grep -E '/$' | awk '{print $4}')" + json_dump + json_cleanup + ;; + "remove") + read -r input + json_load "$input" + json_get_var filename "filename" + json_cleanup + if dirname "$filename" | grep -q ".."; then + echo '{ "result": 255 }' + exit 255 + fi + rm -f "$bg_path/$filename" + echo '{ "result": 0 }' + ;; + "rename") + read -r input + json_load "$input" + json_get_var newname "newname" + json_cleanup + if dirname "$newname" | grep -q ".."; then + echo '{ "result": 255 }' + exit 255 + fi + mkdir -p "$bg_path" + if mv "$tmp_path" "$bg_path/$newname" 2>/dev/null; then + chmod 0644 "$bg_path/$newname" + echo '{ "result": 0 }' + else + echo '{ "result": 1 }' + fi + ;; + "set_mode") + read -r input + json_load "$input" + json_get_var mode "mode" + json_cleanup + case "$mode" in + light|dark|auto) ;; + *) echo '{ "result": 255 }'; exit 255 ;; + esac + if uci -q set fluent.@global[0].mode="$mode" && uci -q commit fluent; then + echo '{ "result": 0 }' + else + echo '{ "result": 1 }' + fi + ;; + esac + ;; +esac diff --git a/luci-theme-fluent-lite/root/usr/share/rpcd/acl.d/luci-theme-fluent.json b/luci-theme-fluent-lite/root/usr/share/rpcd/acl.d/luci-theme-fluent.json new file mode 100644 index 000000000..826bbcb52 --- /dev/null +++ b/luci-theme-fluent-lite/root/usr/share/rpcd/acl.d/luci-theme-fluent.json @@ -0,0 +1,23 @@ +{ + "luci-theme-fluent": { + "description": "Grant UCI and file access for luci-theme-fluent-lite config view", + "read": { + "file": { + "/www/luci-static/fluent/background/*": [ "list" ] + }, + "ubus": { + "luci.fluent": [ "avail", "remove", "rename", "set_mode" ] + }, + "uci": [ "fluent" ] + }, + "write": { + "file": { + "/tmp/fluent_background.tmp": [ "write" ] + }, + "ubus": { + "luci.fluent": [ "set_mode" ] + }, + "uci": [ "fluent" ] + } + } +} diff --git a/luci-theme-fluent-lite/ucode b/luci-theme-fluent-lite/ucode new file mode 120000 index 000000000..68c975fc7 --- /dev/null +++ b/luci-theme-fluent-lite/ucode @@ -0,0 +1 @@ +../luci-theme-fluent/ucode \ No newline at end of file diff --git a/luci-theme-fluent/Makefile b/luci-theme-fluent/Makefile new file mode 100644 index 000000000..519f574cf --- /dev/null +++ b/luci-theme-fluent/Makefile @@ -0,0 +1,42 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=luci-theme-fluent +PKG_VERSION:=1.0.9 +PKG_RELEASE:=1 +PKG_LICENSE:=Apache-2.0 + +LUCI_TITLE:=Fluent Theme for LuCI +LUCI_DEPENDS:=+luci-base +LUCI_MINIFY_CSS:=0 +define Package/$(PKG_NAME)/conffiles +/etc/config/fluent +endef + + +define Package/$(PKG_NAME)/preinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + if command -v apk >/dev/null 2>&1 && apk info -e luci-theme-fluent-lite >/dev/null 2>&1; then + echo "luci-theme-fluent-lite is installed; remove it before installing luci-theme-fluent" >&2 + exit 1 + fi + if command -v opkg >/dev/null 2>&1 && opkg list-installed luci-theme-fluent-lite 2>/dev/null | grep -q '^luci-theme-fluent-lite '; then + echo "luci-theme-fluent-lite is installed; remove it before installing luci-theme-fluent" >&2 + exit 1 + fi +} +endef + +define Package/$(PKG_NAME)/postinst +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] || { + uci batch <<-EOF + set luci.themes.Fluent=/luci-static/fluent + commit luci + EOF + /etc/init.d/rpcd restart +} +endef + +# call BuildPackage - OpenWrt buildroot signature +include $(TOPDIR)/feeds/luci/luci.mk diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/css/fluent.css b/luci-theme-fluent/htdocs/luci-static/fluent/css/fluent.css new file mode 100644 index 000000000..ec430e5bf --- /dev/null +++ b/luci-theme-fluent/htdocs/luci-static/fluent/css/fluent.css @@ -0,0 +1,14723 @@ +.fluent-mac-overlay-card .fluent-mac-overlay-header .fluent-mac-overlay-close, .password-input .toggle-password, .cbi-dropdown.cbi-button.cbi-button-remove, .cbi-dropdown.cbi-button.cbi-button-negative, .cbi-dropdown.btn.cbi-button-remove, .cbi-dropdown.btn.cbi-button-negative, .modal > .modal-close, .modal > .close, .cbi-button.cbi-button-remove, .cbi-button.cbi-button-negative, .cbi-button.danger, .btn.cbi-button-remove, .btn.cbi-button-negative, .btn.danger, :not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child, .cbi-button.cbi-button-link, .btn.cbi-button-link, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button, .cbi-dropdown.cbi-button, .cbi-dropdown.btn, .cbi-image-button, :not(#modal_overlay) > .alert > div:first-of-type .cbi-button, :not(#modal_overlay) > .alert > div:first-of-type .btn, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button, :not(#modal_overlay) > .alert-message > div:first-of-type .btn, .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-reset, .fluent-color-swatch, .cbi-button, .btn, .cbi-button.cbi-button-add, .cbi-button.cbi-button-neutral, .cbi-button.cbi-button-reset, .btn.cbi-button-add, .btn.cbi-button-neutral, .btn.cbi-button-reset, .cbi-button.cbi-button-up, .cbi-button.cbi-button-down, .btn.cbi-button-up, .btn.cbi-button-down, .cbi-button.cbi-button-find, .btn.cbi-button-find, .cbi-dynlist > .add-item > .cbi-button-add, .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-page-actions .cbi-button-apply, .cbi-section .cbi-section-create .cbi-button-add, .cbi-button.cbi-button-save, .cbi-button.cbi-button-apply, .cbi-button.cbi-button-positive, .cbi-button.cbi-button-action, .cbi-button.primary, .cbi-button.important, .btn.cbi-button-save, .btn.cbi-button-apply, .btn.cbi-button-positive, .btn.cbi-button-action, .btn.primary, .btn.important { + height: var(--fluent-btn-height); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + border-radius: var(--fluent-radius-md); + cursor: pointer; + -webkit-user-select: none; + user-select: none; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard); + border: 1px solid #0000; + justify-content: center; + align-items: center; + padding: 0 16px; + line-height: 1; + text-decoration: none; + display: inline-flex; +} + +.cbi-input-file::file-selector-button { + height: var(--fluent-btn-height); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + border-radius: var(--fluent-radius-md); + cursor: pointer; + -webkit-user-select: none; + user-select: none; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard); + border: 1px solid #0000; + justify-content: center; + align-items: center; + padding: 0 16px; + line-height: 1; + text-decoration: none; + display: inline-flex; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header .fluent-mac-overlay-close:disabled, .password-input .toggle-password:disabled, .modal > .modal-close:disabled, .modal > .close:disabled, :not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:disabled:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:disabled:not(.cbi-button):only-child, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn:disabled, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button:disabled, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button:disabled, .cbi-image-button:disabled, :not(#modal_overlay) > .alert > div:first-of-type .cbi-button:disabled, :not(#modal_overlay) > .alert > div:first-of-type .btn:disabled, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button:disabled, :not(#modal_overlay) > .alert-message > div:first-of-type .btn:disabled, .cbi-page-actions .cbi-button-save:disabled, .cbi-page-actions .cbi-button-reset:disabled, .fluent-color-swatch:disabled, .cbi-button:disabled, .btn:disabled, .cbi-dynlist > .add-item > .cbi-button-add:disabled, .cbi-dropdown.cbi-button.cbi-button-apply:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:disabled:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-page-actions .cbi-button-apply:disabled, .cbi-section .cbi-section-create .cbi-button-add:disabled { + color: var(--fluent-text-disabled); + background: var(--fluent-bg-active); + border-color: var(--fluent-bg-active); + cursor: not-allowed; + pointer-events: none; +} + +.cbi-input-file:disabled::file-selector-button { + color: var(--fluent-text-disabled); + background: var(--fluent-bg-active); + border-color: var(--fluent-bg-active); + cursor: not-allowed; + pointer-events: none; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header .fluent-mac-overlay-close:focus-visible, .password-input .toggle-password:focus-visible, .modal > .modal-close:focus-visible, .modal > .close:focus-visible, :not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:focus-visible:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:focus-visible:not(.cbi-button):only-child, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn:focus-visible, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button:focus-visible, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button:focus-visible, .cbi-image-button:focus-visible, :not(#modal_overlay) > .alert > div:first-of-type .cbi-button:focus-visible, :not(#modal_overlay) > .alert > div:first-of-type .btn:focus-visible, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button:focus-visible, :not(#modal_overlay) > .alert-message > div:first-of-type .btn:focus-visible, .cbi-page-actions .cbi-button-save:focus-visible, .cbi-page-actions .cbi-button-reset:focus-visible, .fluent-color-swatch:focus-visible, .cbi-button:focus-visible, .btn:focus-visible, .cbi-dynlist > .add-item > .cbi-button-add:focus-visible, .cbi-dropdown.cbi-button.cbi-button-apply:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:focus-visible:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-page-actions .cbi-button-apply:focus-visible, .cbi-section .cbi-section-create .cbi-button-add:focus-visible { + outline: 2px solid var(--fluent-focus-color); + outline-offset: 2px; +} + +.cbi-input-file:focus-visible::file-selector-button { + outline: 2px solid var(--fluent-focus-color); + outline-offset: 2px; +} + +.cbi-dynlist > .add-item > .cbi-button-add, .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-page-actions .cbi-button-apply, .cbi-section .cbi-section-create .cbi-button-add, .cbi-button.cbi-button-save, .cbi-button.cbi-button-apply, .cbi-button.cbi-button-positive, .cbi-button.cbi-button-action, .cbi-button.primary, .cbi-button.important, .btn.cbi-button-save, .btn.cbi-button-apply, .btn.cbi-button-positive, .btn.cbi-button-action, .btn.primary, .btn.important { + background: var(--fluent-btn-primary-bg); + color: var(--fluent-text-on-primary); + border-color: var(--fluent-btn-primary-bg); +} + +.cbi-dynlist > .add-item > .cbi-button-add:hover, .cbi-dropdown.cbi-button.cbi-button-apply:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:hover:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-page-actions .cbi-button-apply:hover, .cbi-section .cbi-section-create .cbi-button-add:hover, .cbi-button.cbi-button-save:hover, .cbi-button.cbi-button-apply:hover, .cbi-button.cbi-button-positive:hover, .cbi-button.cbi-button-action:hover, .cbi-button.primary:hover, .cbi-button.important:hover, .btn.cbi-button-save:hover, .btn.cbi-button-apply:hover, .btn.cbi-button-positive:hover, .btn.cbi-button-action:hover, .btn.primary:hover, .btn.important:hover { + background: var(--fluent-btn-primary-bg-hover); + border-color: var(--fluent-btn-primary-bg-hover); +} + +.cbi-dynlist > .add-item > .cbi-button-add:active, .cbi-dropdown.cbi-button.cbi-button-apply:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:active:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-page-actions .cbi-button-apply:active, .cbi-section .cbi-section-create .cbi-button-add:active, .cbi-button.cbi-button-save:active, .cbi-button.cbi-button-apply:active, .cbi-button.cbi-button-positive:active, .cbi-button.cbi-button-action:active, .cbi-button.primary:active, .cbi-button.important:active, .btn.cbi-button-save:active, .btn.cbi-button-apply:active, .btn.cbi-button-positive:active, .btn.cbi-button-action:active, .btn.primary:active, .btn.important:active { + background: var(--fluent-btn-primary-bg-active); + border-color: var(--fluent-btn-primary-bg-active); +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button, .cbi-dropdown.cbi-button, .cbi-dropdown.btn, .cbi-image-button, :not(#modal_overlay) > .alert > div:first-of-type .cbi-button, :not(#modal_overlay) > .alert > div:first-of-type .btn, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button, :not(#modal_overlay) > .alert-message > div:first-of-type .btn, .cbi-page-actions .cbi-button-save, .cbi-page-actions .cbi-button-reset, .fluent-color-swatch, .cbi-button, .btn, .cbi-button.cbi-button-add, .cbi-button.cbi-button-neutral, .cbi-button.cbi-button-reset, .btn.cbi-button-add, .btn.cbi-button-neutral, .btn.cbi-button-reset, .cbi-button.cbi-button-up, .cbi-button.cbi-button-down, .btn.cbi-button-up, .btn.cbi-button-down, .cbi-button.cbi-button-find, .btn.cbi-button-find { + background: var(--fluent-btn-secondary-bg); + color: var(--fluent-text); + border-color: var(--fluent-btn-secondary-border); +} + +.cbi-input-file::file-selector-button { + background: var(--fluent-btn-secondary-bg); + color: var(--fluent-text); + border-color: var(--fluent-btn-secondary-border); +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn:hover, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button:hover, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button:hover, .cbi-image-button:hover, :not(#modal_overlay) > .alert > div:first-of-type .cbi-button:hover, :not(#modal_overlay) > .alert > div:first-of-type .btn:hover, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button:hover, :not(#modal_overlay) > .alert-message > div:first-of-type .btn:hover, .cbi-page-actions .cbi-button-save:hover, .cbi-page-actions .cbi-button-reset:hover, .fluent-color-swatch:hover, .cbi-button:hover, .btn:hover { + background: var(--fluent-btn-secondary-bg-hover); + border-color: var(--fluent-btn-secondary-border-hover); +} + +.cbi-input-file:hover::file-selector-button { + background: var(--fluent-btn-secondary-bg-hover); + border-color: var(--fluent-btn-secondary-border-hover); +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn:active, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button:active, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button:active, .cbi-image-button:active, :not(#modal_overlay) > .alert > div:first-of-type .cbi-button:active, :not(#modal_overlay) > .alert > div:first-of-type .btn:active, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button:active, :not(#modal_overlay) > .alert-message > div:first-of-type .btn:active, .cbi-page-actions .cbi-button-save:active, .cbi-page-actions .cbi-button-reset:active, .fluent-color-swatch:active, .cbi-button:active, .btn:active { + background: var(--fluent-btn-secondary-bg-active); + border-color: var(--fluent-btn-secondary-border-active); +} + +.cbi-input-file:active::file-selector-button { + background: var(--fluent-btn-secondary-bg-active); + border-color: var(--fluent-btn-secondary-border-active); +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child, .cbi-button.cbi-button-link, .btn.cbi-button-link { + color: var(--fluent-text); + background: none; + border-color: #0000; +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:hover:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:hover:not(.cbi-button):only-child, .cbi-button.cbi-button-link:hover, .btn.cbi-button-link:hover { + background: var(--fluent-bg-hover); +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:active:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:active:not(.cbi-button):only-child, .cbi-button.cbi-button-link:active, .btn.cbi-button-link:active { + background: var(--fluent-bg-active); +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control, select, select.cbi-input-select, textarea, .cbi-input-textarea, .password-input input, .cbi-dynlist > .add-item > input, cbi-dropdown, .cbi-dropdown, .fluent-custom-select, cbi-dropdown > select, .cbi-dropdown > select, .fluent-custom-select > select, cbi-dropdown > .cbi-input-select, .cbi-dropdown > .cbi-input-select, .fluent-custom-select > .cbi-input-select, .zonebadge > input[type="text"], input[type="text"], input[type="password"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="url"], input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"], .cbi-input-text, .cbi-input-password, .cbi-input-date, .cbi-input-time, .cbi-input-datetime { + height: var(--fluent-input-height); + padding: 0 var(--fluent-space-sm); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-normal); + color: var(--fluent-text); + background: var(--fluent-input-bg); + border: 1px solid var(--fluent-input-border); + border-bottom-width: 1px; + border-bottom-color: var(--fluent-input-border-bottom); + border-radius: var(--fluent-radius-md); + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) { + height: var(--fluent-input-height); + padding: 0 var(--fluent-space-sm); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-normal); + color: var(--fluent-text); + background: var(--fluent-input-bg); + border: 1px solid var(--fluent-input-border); + border-bottom-width: 1px; + border-bottom-color: var(--fluent-input-border-bottom); + border-radius: var(--fluent-radius-md); + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:disabled, select:disabled, textarea:disabled, .cbi-input-textarea:disabled, .password-input input:disabled, .cbi-dynlist > .add-item > input:disabled, cbi-dropdown:disabled, .cbi-dropdown:disabled, .fluent-custom-select:disabled, cbi-dropdown > .cbi-input-select:disabled, .cbi-dropdown > .cbi-input-select:disabled, .fluent-custom-select > .cbi-input-select:disabled, input[type="text"]:disabled, input[type="password"]:disabled, input[type="email"]:disabled, input[type="number"]:disabled, input[type="search"]:disabled, input[type="tel"]:disabled, input[type="url"]:disabled, input[type="date"]:disabled, input[type="time"]:disabled, input[type="datetime-local"]:disabled, input[type="month"]:disabled, input[type="week"]:disabled, .cbi-input-text:disabled, .cbi-input-password:disabled, .cbi-input-date:disabled, .cbi-input-time:disabled, .cbi-input-datetime:disabled { + background: var(--fluent-input-bg-disabled); + color: var(--fluent-text-disabled); + cursor: not-allowed; +} + +.control-group:disabled:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) { + background: var(--fluent-input-bg-disabled); + color: var(--fluent-text-disabled); + cursor: not-allowed; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control::placeholder, select::placeholder, select.cbi-input-select::placeholder, textarea::placeholder, .cbi-input-textarea::placeholder, .password-input input::placeholder, .cbi-dynlist > .add-item > input::placeholder, cbi-dropdown::placeholder, .cbi-dropdown::placeholder, .fluent-custom-select::placeholder, .cbi-dropdown > select::placeholder, .fluent-custom-select > select::placeholder, cbi-dropdown > .cbi-input-select::placeholder, .cbi-dropdown > .cbi-input-select::placeholder, .fluent-custom-select > .cbi-input-select::placeholder, .zonebadge > input[type="text"]::placeholder, input[type="text"]::placeholder, input[type="password"]::placeholder, input[type="email"]::placeholder, input[type="number"]::placeholder, input[type="search"]::placeholder, input[type="tel"]::placeholder, input[type="url"]::placeholder, input[type="date"]::placeholder, input[type="time"]::placeholder, input[type="datetime-local"]::placeholder, input[type="month"]::placeholder, input[type="week"]::placeholder, .cbi-input-text::placeholder, .cbi-input-password::placeholder, .cbi-input-date::placeholder, .cbi-input-time::placeholder, .cbi-input-datetime::placeholder { + color: var(--fluent-text-disabled); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button))::placeholder { + color: var(--fluent-text-disabled); +} + +select, textarea, .cbi-input-textarea, .password-input input, .cbi-dynlist > .add-item > input, cbi-dropdown > select, .cbi-dropdown > select, .fluent-custom-select > select, .zonebadge > input[type="text"], input[type="text"], input[type="password"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="url"], input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"], .cbi-input-text, .cbi-input-password, .cbi-input-date, .cbi-input-time, .cbi-input-datetime { + background-image: linear-gradient(var(--fluent-input-border-bottom-focus), var(--fluent-input-border-bottom-focus)); + transition: background-size var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + background-position: bottom; + background-repeat: no-repeat; + background-size: 0 2px; + background-origin: border-box; +} + +select:hover:not([disabled]):not(:focus), textarea:hover:not([disabled]):not(:focus), .cbi-input-textarea:hover:not([disabled]):not(:focus), .password-input input:hover:not([disabled]):not(:focus), .cbi-dynlist > .add-item > input:hover:not([disabled]):not(:focus), input[type="text"]:hover:not([disabled]):not(:focus), input[type="password"]:hover:not([disabled]):not(:focus), input[type="email"]:hover:not([disabled]):not(:focus), input[type="number"]:hover:not([disabled]):not(:focus), input[type="search"]:hover:not([disabled]):not(:focus), input[type="tel"]:hover:not([disabled]):not(:focus), input[type="url"]:hover:not([disabled]):not(:focus), input[type="date"]:hover:not([disabled]):not(:focus), input[type="time"]:hover:not([disabled]):not(:focus), input[type="datetime-local"]:hover:not([disabled]):not(:focus), input[type="month"]:hover:not([disabled]):not(:focus), input[type="week"]:hover:not([disabled]):not(:focus), .cbi-input-text:hover:not([disabled]):not(:focus), .cbi-input-password:hover:not([disabled]):not(:focus), .cbi-input-date:hover:not([disabled]):not(:focus), .cbi-input-time:hover:not([disabled]):not(:focus), .cbi-input-datetime:hover:not([disabled]):not(:focus) { + border-color: var(--fluent-input-border-hover); + border-bottom-color: var(--fluent-input-border-bottom-hover); +} + +select:focus, textarea:focus, .cbi-input-textarea:focus, .password-input input:focus, .cbi-dynlist > .add-item > input:focus, input[type="text"]:focus, input[type="password"]:focus, input[type="email"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, input[type="url"]:focus, input[type="date"]:focus, input[type="time"]:focus, input[type="datetime-local"]:focus, input[type="month"]:focus, input[type="week"]:focus, .cbi-input-text:focus, .cbi-input-password:focus, .cbi-input-date:focus, .cbi-input-time:focus, .cbi-input-datetime:focus { + border-bottom-color: var(--fluent-input-border-bottom-focus); + background-size: 100% 2px; + outline: none; +} + +:root { + --fluent-font-family: "Segoe UI", "Segoe UI Web (West European)", -apple-system, BlinkMacSystemFont, Roboto, "Helvetica Neue", sans-serif; + --fluent-font-size-base: 14px; + --fluent-font-size-xs: 10px; + --fluent-font-size-sm: 12px; + --fluent-font-size-md: 16px; + --fluent-font-size-lg: 20px; + --fluent-font-size-xl: 24px; + --fluent-font-size-xxl: 28px; + --fluent-font-size-xxxl: 40px; + --fluent-font-weight-regular: 400; + --fluent-font-weight-semibold: 600; + --fluent-font-weight-bold: 700; + --fluent-line-height-tight: 1.2; + --fluent-line-height-normal: 1.43; + --fluent-line-height-relaxed: 1.57; + --fluent-space-unit: 4px; + --fluent-space-xxs: 2px; + --fluent-space-xs: 4px; + --fluent-space-sm: 8px; + --fluent-space-md: 12px; + --fluent-space-lg: 16px; + --fluent-space-xl: 24px; + --fluent-space-xxl: 32px; + --fluent-space-xxxl: 48px; + --fluent-radius-sm: 2px; + --fluent-radius-md: 4px; + --fluent-radius-lg: 8px; + --fluent-radius-xl: 12px; + --fluent-radius-circular: 10000px; + --fluent-radius-round: 50%; + --fluent-duration-fast: .15s; + --fluent-duration-normal: .25s; + --fluent-duration-slow: .4s; + --fluent-duration-slower: .5s; + --fluent-easing-standard: cubic-bezier(.8, 0, .2, 1); + --fluent-easing-decelerate: cubic-bezier(0, 0, 0, 1); + --fluent-easing-accelerate: cubic-bezier(1, 0, 1, 1); + --fluent-easing-easy-ease: cubic-bezier(.33, 0, .67, 1); + --fluent-zindex-actions: 1000; + --fluent-zindex-modal: 1100; + --fluent-zindex-tooltip: 1200; + --fluent-zindex-toast: 1300; + --fluent-zindex-dropdown: 1500; + --fluent-zindex-menu: 1050; + --fluent-sidebar-width: 260px; + --fluent-sidebar-collapsed-width: 48px; + --fluent-header-height: 48px; + --fluent-control-height: 42px; + --fluent-btn-height: var(--fluent-control-height, 42px); + --fluent-btn-padding: 0 16px; + --fluent-input-height: var(--fluent-control-height, 42px); + --fluent-card-radius: var(--fluent-radius-lg); + --fluent-card-padding: var(--fluent-space-lg); + --fluent-modal-radius: var(--fluent-radius-lg); + --fluent-modal-padding: var(--fluent-space-xl); +} + +@media (max-width: 767px) { + :root { + --fluent-modal-padding: var(--fluent-space-lg); + } +} + +:root { + --fluent-tab-height: 44px; + --fluent-tab-indicator-height: 3px; + --fluent-checkbox-size: 20px; + --fluent-switch-width: 40px; + --fluent-switch-height: 20px; + --fluent-switch-thumb-size: 12px; + --fluent-switch-thumb-offset: 3px; + --fluent-scrollbar-width: 6px; + --fluent-scrollbar-track-bg: transparent; + --fluent-progress-height: 4px; + --fluent-badge-radius: 10px; + --fluent-zonebadge-text: #111; + --fluent-toast-radius: var(--fluent-radius-lg); + --fluent-dropdown-max-height-limit: 350px; + --fluent-dropdown-max-height: var(--fluent-dropdown-max-height-limit); + --fluent-brand-10: color-mix(in srgb, var(--fluent-primary) 10%, #000); + --fluent-brand-20: color-mix(in srgb, var(--fluent-primary) 20%, #000); + --fluent-brand-30: color-mix(in srgb, var(--fluent-primary) 30%, #000); + --fluent-brand-40: color-mix(in srgb, var(--fluent-primary) 40%, #000); + --fluent-brand-50: color-mix(in srgb, var(--fluent-primary) 50%, #000); + --fluent-brand-60: color-mix(in srgb, var(--fluent-primary) 65%, #000); + --fluent-brand-70: color-mix(in srgb, var(--fluent-primary) 80%, #000); + --fluent-brand-80: var(--fluent-primary); + --fluent-brand-90: color-mix(in srgb, var(--fluent-primary) 90%, #fff); + --fluent-brand-100: color-mix(in srgb, var(--fluent-primary) 80%, #fff); + --fluent-brand-110: color-mix(in srgb, var(--fluent-primary) 70%, #fff); + --fluent-brand-120: color-mix(in srgb, var(--fluent-primary) 60%, #fff); + --fluent-brand-130: color-mix(in srgb, var(--fluent-primary) 45%, #fff); + --fluent-brand-140: color-mix(in srgb, var(--fluent-primary) 30%, #fff); + --fluent-brand-150: color-mix(in srgb, var(--fluent-primary) 15%, #fff); + --fluent-brand-160: color-mix(in srgb, var(--fluent-primary) 5%, #fff); +} + +*, :before, :after { + box-sizing: border-box; + -webkit-tap-highlight-color: #0000; + margin: 0; + padding: 0; +} + +html { + -moz-text-size-adjust: 100%; + text-size-adjust: 100%; + font-size: 16px; +} + +body { + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + line-height: var(--fluent-line-height-normal); + color: var(--fluent-text); + background: var(--fluent-bg-page); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +h1, h2, h3, h4, h5, h6 { + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-tight); + color: var(--fluent-text); +} + +h1 { + font-size: var(--fluent-font-size-xxxl); +} + +h2 { + font-size: var(--fluent-font-size-xxl); +} + +h3 { + font-size: var(--fluent-font-size-xl); +} + +h4 { + font-size: var(--fluent-font-size-lg); +} + +h5 { + font-size: var(--fluent-font-size-md); +} + +h6 { + font-size: var(--fluent-font-size-base); +} + +p { + margin-bottom: var(--fluent-space-lg); +} + +a { + color: var(--fluent-text-link); + text-decoration: none; +} + +a:hover { + color: var(--fluent-text-link-hover); + text-decoration: underline; +} + +ul, ol { + padding-inline-start: var(--fluent-space-xl); +} + +li { + margin-bottom: var(--fluent-space-xxs); +} + +img { + vertical-align: middle; + max-width: 100%; + height: auto; +} + +table { + border-collapse: collapse; + width: 100%; +} + +button, input, optgroup, select, textarea { + font-family: inherit; + font-size: 100%; + line-height: inherit; + color: inherit; + margin: 0; +} + +button, input { + overflow: visible; +} + +button, select { + text-transform: none; +} + +button, [type="button"], [type="reset"], [type="submit"] { + appearance: button; +} + +textarea { + resize: vertical; + overflow: auto; +} + +::selection { + background: var(--fluent-selection-bg); + color: var(--fluent-selection-text); +} + +:focus-visible { + outline: 2px solid var(--fluent-focus-color); + outline-offset: 2px; +} + +body::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +body::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +body::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +body::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +body::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +body::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +body::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +body::-webkit-scrollbar-corner { + background: none; + display: none; +} + +body::-webkit-resizer { + background: none; +} + +.sr-only { + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + position: absolute; + overflow: hidden; +} + +.text-truncate { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.text-left, .left { + text-align: start; +} + +.text-center { + text-align: center; +} + +.text-right, .right { + text-align: end; +} + +.pull-right { + float: inline-end; +} + +.pull-left { + float: inline-start; +} + +.hidden { + display: none !important; +} + +.visible { + display: block; +} + +.nowrap { + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +@media (max-width: 575.98px) { + .hide-xs { + display: none !important; + } +} + +@media (max-width: 767px) { + .hide-sm { + display: none !important; + } +} + +@media (max-width: 991.98px) { + .hide-md { + display: none !important; + } +} + +@media (max-width: 1199.98px) { + .hide-lg { + display: none !important; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes slideInUp { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInDown { + from { + opacity: 0; + transform: translateY(-20px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes slideInLeft { + from { + opacity: 0; + transform: translateX(-20px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(20px); + } + + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes spin { + from { + transform: rotate(0); + } + + to { + transform: rotate(360deg); + } +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + + 50% { + opacity: .5; + } +} + +@keyframes fluent-spin-swing { + 0% { + transform: translateY(-50%) rotate(-135deg); + } + + 50% { + transform: translateY(-50%) rotate(0); + } + + 100% { + transform: translateY(-50%) rotate(225deg); + } +} + +div:not([id="view"]) > .spinning { + position: relative; + padding-inline-start: 40px !important; +} + +div:not([id="view"]) > .spinning:before { + inset-inline-start: 10px; + content: ""; + --duration: 1s; + --indicator-size: 3px; + --size: 24px; + height: var(--size); + width: var(--size); + box-sizing: border-box; + border: var(--indicator-size) solid #0078d433; + border-top-color: var(--fluent-primary); + border-right-color: var(--fluent-primary); + animation: fluent-spin-swing var(--duration) infinite ease; + background: none; + border-radius: 50%; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +@keyframes fluent-spin-swing-clean { + 0% { + transform: rotate(-135deg); + } + + 50% { + transform: rotate(0); + } + + 100% { + transform: rotate(225deg); + } +} + +div[id="view"] > .spinning { + box-sizing: border-box; + text-align: center; + min-height: 400px; + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-md, 16px); + color: var(--fluent-text-secondary); + justify-content: center; + align-items: center; + gap: var(--fluent-space-md, 12px); + padding: var(--fluent-space-xxl) 0; + flex-direction: column; + display: flex; +} + +div[id="view"] > .spinning:before { + content: ""; + --spinner-size: 48px; + --indicator-size: 4px; + height: var(--spinner-size); + width: var(--spinner-size); + box-sizing: border-box; + border: var(--indicator-size) solid #0078d426; + border-top-color: var(--fluent-primary); + border-right-color: var(--fluent-primary); + margin-bottom: var(--fluent-space-sm, 8px); + background: none; + border-radius: 50%; + animation: 1.5s infinite fluent-spin-swing-clean; + display: block; + position: static; +} + +@keyframes fluent-view-fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +div[id="view"] > :not(.spinning) { + animation: fluent-view-fade-in var(--fluent-duration-slow) var(--fluent-easing-decelerate) forwards; +} + +@keyframes fluent-login-fade-in { + from { + opacity: 0; + transform: translateY(24px) scale(.985); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes fluent-tooltip-show { + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +:root { + --background-color-high: var(--fluent-card-bg); + --background-color-medium: var(--fluent-bg-active); + --background-color-low: var(--fluent-table-header-bg); + --text-color-highest: var(--fluent-text); + --text-color-high: var(--fluent-text); + --text-color-medium: var(--fluent-text-secondary); + --text-color-low: var(--fluent-text-disabled); + --border-color-high: var(--fluent-border-hover); + --border-color-medium: var(--fluent-border); + --border-color-low: var(--fluent-table-border); + --primary-color-high: var(--fluent-primary); + --primary-color-medium: var(--fluent-primary-hover); + --primary-color-low: var(--fluent-primary-active); + --on-primary-color: var(--fluent-text-on-primary); + --error-color-high-rgb: 209, 52, 56; + --error-color-high: var(--fluent-error); + --error-color-medium: var(--fluent-error-hover); + --error-color-low: var(--fluent-error-active); + --on-error-color: var(--fluent-text-on-primary); + --success-color-high-rgb: 16, 124, 16; + --success-color-high: var(--fluent-success); + --success-color-medium: var(--fluent-success); + --success-color-low: var(--fluent-success); + --on-success-color: var(--fluent-text-on-primary); + --warn-color-high: var(--fluent-warning); + --warn-color-medium: var(--fluent-warning); + --warn-color-low: var(--fluent-warning); + --on-warn-color: var(--fluent-text); + --font-sans: var(--fluent-font-family); + --font-mono: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + --disabled-opacity: .7; +} + +[data-theme="dark"], :root[data-darkmode="true"], [data-darkmode="true"] { + --error-color-high-rgb: 255, 95, 95; + --success-color-high-rgb: 111, 235, 111; + --warn-color-medium: var(--fluent-warning-hover); + --warn-color-low: var(--fluent-warning-active); + --on-warn-color: var(--fluent-card-bg); + --disabled-opacity: .4; +} + +.cbi-button.cbi-button-remove, .cbi-button.cbi-button-negative, .cbi-button.danger, .btn.cbi-button-remove, .btn.cbi-button-negative, .btn.danger { + background: var(--fluent-error); + color: var(--fluent-text-on-primary); + border-color: var(--fluent-error); +} + +.cbi-button.cbi-button-remove:hover, .cbi-button.cbi-button-negative:hover, .cbi-button.danger:hover, .btn.cbi-button-remove:hover, .btn.cbi-button-negative:hover, .btn.danger:hover { + background: var(--fluent-error-hover); + border-color: var(--fluent-error-hover); +} + +.cbi-button.cbi-button-remove:active, .cbi-button.cbi-button-negative:active, .cbi-button.danger:active, .btn.cbi-button-remove:active, .btn.cbi-button-negative:active, .btn.danger:active { + background: var(--fluent-error-active); + border-color: var(--fluent-error-active); +} + +.cbi-button.cbi-button-link, .btn.cbi-button-link { + color: var(--fluent-text-link); +} + +.cbi-button.cbi-button-link:hover, .btn.cbi-button-link:hover { + color: var(--fluent-text-link-hover); + text-decoration: underline; +} + +.cbi-button.cbi-button-up, .cbi-button.cbi-button-down, .btn.cbi-button-up, .btn.cbi-button-down { + width: var(--fluent-btn-height); + padding: 0; + font-size: 0; +} + +.cbi-button.cbi-button-up:before, .cbi-button.cbi-button-down:before, .btn.cbi-button-up:before, .btn.cbi-button-down:before { + content: ""; + border-right: 2px solid var(--fluent-text-secondary); + border-bottom: 2px solid var(--fluent-text-secondary); + width: 8px; + height: 8px; + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + display: block; +} + +.cbi-button.cbi-button-up:hover:before, .cbi-button.cbi-button-down:hover:before, .btn.cbi-button-up:hover:before, .btn.cbi-button-down:hover:before { + border-color: var(--fluent-text); +} + +.cbi-button.cbi-button-up:before, .btn.cbi-button-up:before { + margin-top: 3px; + transform: rotate(-135deg); +} + +.cbi-button.cbi-button-down:before, .btn.cbi-button-down:before { + margin-bottom: 3px; + transform: rotate(45deg); +} + +.cbi-button.cbi-button-find:before, .btn.cbi-button-find:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-arrow-right-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-arrow-right-20-regular); + mask-image: var(--fluent-mask-arrow-right-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.cbi-button.spinning, .btn.spinning { + position: relative; + pointer-events: none !important; + padding-inline-start: 36px !important; +} + +.cbi-button.spinning:before, .btn.spinning:before { + content: ""; + inset-inline-start: 12px; + --size: 16px; + --indicator-size: 2px; + height: var(--size); + width: var(--size); + box-sizing: border-box; + border: var(--indicator-size) solid color-mix(in srgb, currentColor 20%, transparent); + background: none; + border-top-color: currentColor; + border-right-color: currentColor; + border-radius: 50%; + animation: 1s infinite fluent-spin-swing; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.cbi-button:disabled, .cbi-button[disabled], .btn:disabled, .btn[disabled] { + background: var(--fluent-bg-active); + color: var(--fluent-text-disabled); + border-color: var(--fluent-bg-active); + cursor: not-allowed; + pointer-events: none; +} + +.cbi-button svg, .cbi-button img { + width: 16px; + height: 16px; + margin-right: var(--fluent-space-xs); + flex-shrink: 0; +} + +.cbi-button-up + .cbi-button-down { + margin-left: 2px; +} + +.btns:has( > .cbi-button) { + gap: var(--fluent-space-sm); + margin-bottom: var(--fluent-space-md); + display: inline-flex; +} + +.control-group:not(:has( > :not(.cbi-button))) { + flex-wrap: wrap; + align-items: center; + gap: 8px; + display: flex; +} + +input[type="text"], input[type="password"], input[type="email"], input[type="number"], input[type="search"], input[type="tel"], input[type="url"], input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"], .cbi-input-text, .cbi-input-password, .cbi-input-date, .cbi-input-time, .cbi-input-datetime { + box-sizing: border-box; + max-width: 100%; +} + +.cbi-input-group { + align-items: center; + gap: var(--fluent-space-xs); + display: flex; +} + +.fluent-color-field { + align-items: center; + gap: var(--fluent-space-sm); + width: min(100%, 420px); + display: flex; +} + +.fluent-color-field > .fluent-color-field__text { + flex: auto; + min-width: 0; +} + +.fluent-color-swatch { + width: var(--fluent-input-height); + min-width: var(--fluent-input-height); + cursor: pointer; + flex-shrink: 0; + padding: 0; + position: relative; + overflow: hidden; +} + +.fluent-color-swatch:focus-within { + outline: 2px solid var(--fluent-focus-color); + outline-offset: 2px; +} + +.fluent-color-swatch__input { + opacity: 0; + cursor: pointer; + position: absolute; + inset: 0; +} + +.fluent-color-swatch__preview { + width: calc(var(--fluent-input-height) - var(--fluent-space-xs)*2); + height: calc(var(--fluent-input-height) - var(--fluent-space-xs)*2); + border-radius: var(--fluent-radius-sm); + border: 1px solid var(--fluent-input-border); + pointer-events: none; + box-sizing: border-box; +} + +.fluent-color-swatch__input:disabled + .fluent-color-swatch__preview { + opacity: .5; +} + +input:disabled, .cbi-input-text:disabled, .cbi-input-password:disabled, .cbi-input-date:disabled, .cbi-input-time:disabled, .cbi-input-datetime:disabled { + background-color: var(--fluent-input-bg-disabled); + color: var(--fluent-text-disabled); + cursor: not-allowed; + border-color: var(--fluent-border); + border-bottom-color: var(--fluent-border); +} + +input[type="search"] { + appearance: none; + padding-inline-start: var(--fluent-space-xl); +} + +input[type="search"]::-webkit-search-cancel-button { + appearance: none; + cursor: pointer; + background: none; + border: none; + width: 14px; + height: 14px; +} + +input[type="date"]::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +input[type="time"]::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +input[type="datetime-local"]::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +input[type="month"]::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +input[type="week"]::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.cbi-input-date::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.cbi-input-time::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.cbi-input-datetime::-webkit-calendar-picker-indicator { + cursor: pointer; + opacity: .6; + filter: invert(var(--fluent-calendar-picker-invert, 0)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +input[type="date"]::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +input[type="time"]::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +input[type="datetime-local"]::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +input[type="month"]::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +input[type="week"]::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +.cbi-input-date::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +.cbi-input-time::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +.cbi-input-datetime::-webkit-calendar-picker-indicator:hover { + opacity: .9; +} + +.cbi-section { + background: var(--fluent-card-bg); + border-radius: var(--fluent-card-radius); + box-shadow: var(--fluent-card-shadow); + padding: var(--fluent-card-padding); + box-sizing: border-box; + border: none; + width: 100%; + min-width: 0; + max-width: 100%; + position: relative; +} + +.cbi-section > * .cbi-section { + box-shadow: none; + padding: 0; +} + +.cbi-section .cbi-section-node { + max-width: 100%; +} + +.cbi-section .cbi-section-node:not(:first-child) { + padding-top: var(--fluent-space-lg); +} + +.cbi-section .cbi-section-remove.right + .cbi-section-node:has( + .cbi-section-remove.right + .cbi-section-node) { + border-bottom: 2px solid var(--fluent-card-border); + margin-bottom: 8px; + padding-bottom: 8px; +} + +.cbi-section .cbi-section-remove { + float: inline-end; + z-index: 10; + margin-block-start: 8px; + margin-inline: 0 8px; + position: relative; +} + +.cbi-section .cbi-section-remove .cbi-button { + background: var(--fluent-error); + border-color: var(--fluent-error); + color: var(--fluent-button-text); +} + +.cbi-section .cbi-section-remove .cbi-button:hover, .cbi-section .cbi-section-remove .cbi-button:focus { + background: var(--fluent-error-dark); + border-color: var(--fluent-error-dark); +} + +.cbi-section .cbi-section-create { + margin-top: var(--fluent-space-sm); + justify-content: flex-end; + gap: var(--fluent-space-sm); + margin-inline-end: var(--fluent-space-sm); + margin-bottom: var(--fluent-space-sm); + display: flex; +} + +.cbi-section-error { + background: var(--fluent-error-light); + border: 1px solid var(--fluent-error); + border-radius: var(--fluent-card-radius); + padding: var(--fluent-space-lg); + margin-bottom: var(--fluent-space-lg); + color: var(--fluent-error); +} + +.cbi-section-table-titles h3 { + font-size: var(--fluent-font-size-md); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + padding: var(--fluent-space-lg) 0; + margin: 0; +} + +.panel { + background: var(--fluent-card-bg); + border-radius: var(--fluent-card-radius); + box-shadow: var(--fluent-card-shadow); + padding: var(--fluent-card-padding); + margin-bottom: var(--fluent-space-lg); + border: none; +} + +.panel .panel-title { + font-size: var(--fluent-font-size-md); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + padding-bottom: var(--fluent-space-sm); + margin-bottom: var(--fluent-space-lg); + border-bottom: 1px solid var(--fluent-card-border); +} + +#cbi-network > .cbi-section-node { + padding: 0; +} + +.cbi-section-actions:not(.th):not(.td) { + justify-content: flex-end; + align-items: center; + gap: var(--fluent-space-xs); + padding: var(--fluent-space-lg) 0 0; + border-top: 1px solid var(--fluent-card-border); + margin-top: var(--fluent-space-lg); + display: flex; +} + +.table { + border-collapse: collapse; + width: 100%; +} + +.table th { + background: var(--fluent-table-header-bg); + font-weight: var(--fluent-font-weight-semibold); + text-align: center; +} + +.table td, .table th { + padding: var(--fluent-space-xs) var(--fluent-space-sm); + border-bottom: 1px solid var(--fluent-table-border); +} + +.table tr:hover td { + background: var(--fluent-table-row-hover); +} + +.cbi-tblsection, .cbi-section.cbi-tblsection { + box-sizing: border-box; + max-width: 100%; + overflow: visible; +} + +.tr { + border-bottom: 1px solid var(--fluent-table-border); + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.tr:hover { + background: var(--fluent-table-row-hover); +} + +.td, .th { + padding: var(--fluent-space-xs) var(--fluent-space-sm); + font-size: var(--fluent-font-size-base); + color: var(--fluent-text); + line-height: var(--fluent-line-height-normal); + text-align: center; + vertical-align: middle; + word-break: normal; + overflow-wrap: normal; + min-width: 120px; + max-width: none; +} + +.td.nowrap, .td[data-widget="CBI.ButtonValue"], .td[data-widget="CBI.FlagValue"], .td.cbi-section-actions, .th.nowrap, .th[data-widget="CBI.ButtonValue"], .th[data-widget="CBI.FlagValue"], .th.cbi-section-actions { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.td:has(.ifacebadge) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.td:has(input) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.td:has(.cbi-button) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.td:has(.btn) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.td:has(button) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.th:has(.ifacebadge) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.th:has(input) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.th:has(.cbi-button) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.th:has(.btn) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.th:has(button) { + min-width: auto; + max-width: none; + white-space: nowrap !important; + word-break: normal !important; + overflow-wrap: normal !important; +} + +.td .cell-content, .td .td-content, .th .cell-content, .th .td-content { + aspect-ratio: 1; + max-height: 100%; + overflow: hidden; +} + +.td.left, .th.left { + text-align: start; + justify-content: flex-start; +} + +.td.right, .th.right { + text-align: end; + justify-content: flex-end; +} + +.td.center, .th.center { + text-align: center; + justify-content: center; +} + +div.tr { + align-items: stretch; + display: flex; +} + +div.td, div.th { + justify-content: center; + align-items: center; + gap: var(--fluent-space-xs); + flex-wrap: nowrap; + flex: auto; + display: flex; +} + +.th { + background: var(--fluent-table-header-bg); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + border-bottom: 2px solid var(--fluent-table-border); + white-space: nowrap; +} + +.cbi-section-table { + border-collapse: collapse; + width: 100%; +} + +.cbi-section-table .tr.cbi-section-table-titles .th { + font-size: var(--fluent-font-size-sm); + text-transform: uppercase; + letter-spacing: .04em; + color: var(--fluent-text-secondary); + background: var(--fluent-table-header-bg); + border-bottom: 2px solid var(--fluent-table-border); +} + +.cbi-section-table .tr.cbi-section-table-row { + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.cbi-section-table .tr.cbi-section-table-row:hover { + background: var(--fluent-table-row-hover); +} + +.cbi-section-table .tr.cbi-section-table-row .td { + padding: var(--fluent-space-xs) var(--fluent-space-sm); +} + +.cbi-section-table:has(.cbi-section-table-titles.named):not(:has(td.cbi-section-table-titles)) .tr.cbi-section-table-titles.named:before { + content: attr(data-title); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + font-size: var(--fluent-font-size-sm); + text-transform: uppercase; + letter-spacing: .04em; + color: var(--fluent-text-secondary); + background: var(--fluent-table-header-bg); + border-bottom: 2px solid var(--fluent-table-border); + font-weight: var(--fluent-font-weight-semibold); + white-space: nowrap; + vertical-align: middle; + text-align: center; + display: table-cell; +} + +.cbi-section-table:has(.cbi-section-table-titles.named):not(:has(td.cbi-section-table-titles)) .tr.cbi-section-table-row[data-title]:before { + content: attr(data-title); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + font-size: var(--fluent-font-size-sm); + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-semibold); + white-space: nowrap; + vertical-align: middle; + text-align: start; + border-bottom: 1px solid var(--fluent-table-border); + display: table-cell; +} + +.tr.placeholder { + padding: var(--fluent-space-lg); + text-align: center; + color: var(--fluent-text-disabled); + font-style: italic; +} + +.cbi-section-table .cbi-section-actions, td.cbi-section-actions { + padding: var(--fluent-space-xs) var(--fluent-space-sm); + white-space: nowrap; + text-align: end; + vertical-align: middle; + align-self: center !important; +} + +.cbi-section-table .cbi-section-actions > div, td.cbi-section-actions > div { + flex-wrap: nowrap; + justify-content: flex-end; + align-items: center; + gap: 5px; + display: flex; +} + +.table-wrapper { + -webkit-overflow-scrolling: touch; + box-sizing: border-box; + width: 100%; + min-width: 0; + max-width: 100%; + display: block; + overflow-x: auto; +} + +.table-wrapper > table.table, .table-wrapper > table.cbi-section-table, .table-wrapper > div.table, .table-wrapper > div.cbi-section-table { + table-layout: auto; + border-collapse: collapse; + box-sizing: border-box; + width: 100%; + min-width: 100%; + max-width: none; + display: table !important; +} + +.table-wrapper > div.table > .thead, .table-wrapper > div.cbi-section-table > .thead { + display: table-header-group; +} + +.table-wrapper > div.table > .tbody, .table-wrapper > div.cbi-section-table > .tbody { + display: table-row-group; +} + +.table-wrapper > div.table > .tfoot, .table-wrapper > div.cbi-section-table > .tfoot { + display: table-footer-group; +} + +.table-wrapper > div.table > .tr, .table-wrapper > div.table > .thead > .tr, .table-wrapper > div.table > .tbody > .tr, .table-wrapper > div.table > .tfoot > .tr, .table-wrapper > div.cbi-section-table > .tr, .table-wrapper > div.cbi-section-table > .thead > .tr, .table-wrapper > div.cbi-section-table > .tbody > .tr, .table-wrapper > div.cbi-section-table > .tfoot > .tr { + display: table-row !important; +} + +.table-wrapper > div.table > .tr > .td, .table-wrapper > div.table > .tr > .th, .table-wrapper > div.table > .thead > .tr > .td, .table-wrapper > div.table > .thead > .tr > .th, .table-wrapper > div.table > .tbody > .tr > .td, .table-wrapper > div.table > .tbody > .tr > .th, .table-wrapper > div.table > .tfoot > .tr > .td, .table-wrapper > div.table > .tfoot > .tr > .th, .table-wrapper > div.cbi-section-table > .tr > .td, .table-wrapper > div.cbi-section-table > .tr > .th, .table-wrapper > div.cbi-section-table > .thead > .tr > .td, .table-wrapper > div.cbi-section-table > .thead > .tr > .th, .table-wrapper > div.cbi-section-table > .tbody > .tr > .td, .table-wrapper > div.cbi-section-table > .tbody > .tr > .th, .table-wrapper > div.cbi-section-table > .tfoot > .tr > .td, .table-wrapper > div.cbi-section-table > .tfoot > .tr > .th { + display: table-cell !important; +} + +.table-wrapper > div.table .tr[data-title]:before, .table-wrapper > div.table .td[data-title]:before, .table-wrapper > div.cbi-section-table .tr[data-title]:before, .table-wrapper > div.cbi-section-table .td[data-title]:before { + content: none; +} + +#modal_overlay { + top: 0; + background: var(--fluent-modal-overlay); + width: 100%; + height: 100%; + z-index: var(--fluent-zindex-modal); + -webkit-overflow-scrolling: touch; + padding: var(--fluent-space-md); + box-sizing: border-box; + justify-content: center; + align-items: safe center; + display: none; + position: fixed; + inset-inline-start: 0; + overflow-y: auto; +} + +.modal { + background: var(--fluent-modal-bg); + border-radius: var(--fluent-modal-radius); + box-shadow: var(--fluent-shadow-xl); + padding: var(--fluent-modal-padding); + width: 90%; + min-width: 280px; + max-width: 800px; + max-height: 85dvh; + animation: slideInUp var(--fluent-duration-normal) var(--fluent-easing-decelerate); + flex-direction: column; + display: flex; + position: relative; + overflow: hidden; +} + +:is(.modal:has( > .alert-message), .modal:has( > .alert)) { + max-width: 600px; +} + +.modal .alert, .modal .alert-message { + display: block; + padding-block: 0 !important; + padding-inline: 0 !important; + box-shadow: none !important; + color: var(--fluent-text) !important; + background: none !important; + border: none !important; + margin: 0 !important; + padding-inline-start: calc(18px + var(--fluent-space-md)) !important; +} + +.modal .alert:before, .modal .alert-message:before { + inset-inline-start: 0 !important; + margin-top: 2px !important; + position: absolute !important; + top: 0 !important; +} + +.modal h4, .modal .modal-header { + background: var(--fluent-modal-bg); + padding-bottom: var(--fluent-space-md); + font-size: var(--fluent-font-size-lg); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + flex: none; + margin: 0; +} + +.modal h4 h3, .modal .modal-header h3 { + font-size: var(--fluent-font-size-lg); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + margin: 0; +} + +.modal .button-row, .modal .modal-footer, .modal > .right { + background: var(--fluent-modal-bg); + padding-top: var(--fluent-space-md); + justify-content: flex-start; + align-items: center; + gap: var(--fluent-space-sm); + flex-flow: row-reverse wrap; + flex: none; + margin: 0; + display: flex; +} + +.modal .modal-content-wrap, .modal > .cbi-map { + padding-bottom: var(--fluent-space-xs); + overflow-y: auto; + overflow-y: overlay; + flex: auto; + min-height: 20px; +} + +.modal .modal-content-wrap::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.modal > .cbi-map::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.modal .modal-content-wrap::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.modal > .cbi-map::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.modal .modal-content-wrap::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.modal > .cbi-map::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.modal .modal-content-wrap::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.modal > .cbi-map::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.modal .modal-content-wrap::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.modal > .cbi-map::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.modal .modal-content-wrap::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.modal > .cbi-map::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.modal .modal-content-wrap::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.modal > .cbi-map::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.modal .modal-content-wrap::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.modal > .cbi-map::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.modal .modal-content-wrap::-webkit-resizer { + background: none; +} + +.modal > .cbi-map::-webkit-resizer { + background: none; +} + +.modal .modal-content-wrap > * { + margin-bottom: var(--fluent-space-md); +} + +.modal .modal-content-wrap > :last-child { + margin-bottom: 0; +} + +.modal .cbi-map .cbi-section { + box-shadow: none; + padding: 0 !important; +} + +.modal .modal-body { + margin-bottom: var(--fluent-space-lg); + font-size: var(--fluent-font-size-base); + color: var(--fluent-text); + line-height: var(--fluent-line-height-normal); +} + +.modal > .modal-close, .modal > .close { + width: 32px; + height: 32px; + color: var(--fluent-text-secondary); + top: var(--fluent-space-lg); + z-index: 11; + background: none; + border: none; + padding: 0; + position: absolute; + inset-inline-end: var(--fluent-space-lg); +} + +.modal > .modal-close:before, .modal > .close:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-dismiss-20-regular: url("data:image/svg+xml,"); + width: 20px; + height: 20px; + -webkit-mask-image: var(--fluent-mask-dismiss-20-regular); + mask-image: var(--fluent-mask-dismiss-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.modal > .modal-close:hover, .modal > .close:hover { + background: var(--fluent-bg-hover); + color: var(--fluent-text); +} + +.modal:not(.spinning) .modal-content-wrap > p.spinning { + margin-top: var(--fluent-space-sm); + margin-bottom: var(--fluent-space-sm); + overflow: visible !important; +} + +.modal > :not(.right) .right { + justify-content: flex-end; + align-items: center; + gap: var(--fluent-space-xs); + display: flex; +} + +.modal-wide { + max-width: 1000px; +} + +.modal, .uci-dialog { + padding: var(--fluent-modal-padding); +} + +.modal .button-row > .btn, .uci-dialog .button-row > .btn { + min-width: 96px; +} + +.modal .button-row .cbi-button + .cbi-button, .uci-dialog .button-row .cbi-button + .cbi-button { + margin-inline-start: var(--fluent-space-sm); +} + +.modal > .cbi-section, .uci-dialog > .cbi-section { + box-shadow: none !important; + background: none !important; + border: none !important; + margin: 0 !important; + padding: 0 !important; + overflow: visible !important; +} + +body.modal-overlay-active { + height: 100vh; + overflow: hidden; +} + +body.modal-overlay-active #modal_overlay { + animation: fadeIn var(--fluent-duration-normal) var(--fluent-easing-decelerate); + display: flex; +} + +:is(body.modal-overlay-active:has(#modal_overlay .modal.notice), body.modal-overlay-active:has(#modal_overlay .modal.spinning)) { + height: auto !important; + overflow: auto !important; +} + +:is(body.modal-overlay-active:has(#modal_overlay .modal.notice) .modal, body.modal-overlay-active:has(#modal_overlay .modal.notice) .uci-dialog, body.modal-overlay-active:has(#modal_overlay .modal.spinning) .modal, body.modal-overlay-active:has(#modal_overlay .modal.spinning) .uci-dialog) { + padding: var(--fluent-space-md); +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent button { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent input { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent select { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent textarea { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent label { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .cbi-button { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .cbi-dropdown { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .fluent-custom-select { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .cbi-checkbox { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .fluent-menu-editor__drag-handle { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent a.btn { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent button { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent input { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent select { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent textarea { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent label { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .cbi-button { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .cbi-dropdown { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .fluent-custom-select { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .cbi-checkbox { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .fluent-menu-editor__drag-handle { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent a.btn { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent button * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent input * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent select * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent textarea * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent label * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .cbi-button * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .cbi-dropdown * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .fluent-custom-select * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .cbi-checkbox * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent .fluent-menu-editor__drag-handle * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.notice) #maincontent a.btn * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent button * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent input * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent select * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent textarea * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent label * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .cbi-button * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .cbi-dropdown * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .fluent-custom-select * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .cbi-checkbox * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent .fluent-menu-editor__drag-handle * { + pointer-events: none !important; +} + +body.modal-overlay-active:has(#modal_overlay .modal.spinning) #maincontent a.btn * { + pointer-events: none !important; +} + +:is(body.modal-overlay-active:has(#modal_overlay .modal.notice) #indicators, body.modal-overlay-active:has(#modal_overlay .modal.spinning) #indicators) { + pointer-events: none !important; + cursor: not-allowed !important; + filter: grayscale(.5) opacity(.6) !important; +} + +:is(body.modal-overlay-active:has(#modal_overlay .modal.notice) #indicators *, body.modal-overlay-active:has(#modal_overlay .modal.spinning) #indicators *) { + pointer-events: none !important; +} + +:is(body.modal-overlay-active #modal_overlay:has(.modal.notice), body.modal-overlay-active #modal_overlay:has(.modal.spinning)) { + pointer-events: none !important; + padding-top: var(--fluent-space-xl) !important; + background: none !important; + align-items: flex-start !important; + display: flex !important; +} + +:is(.modal.alert-message:not(:has(.button-row)):not(:has(button)), .modal.spinning) { + align-items: center !important; + gap: var(--fluent-space-md) !important; + pointer-events: auto !important; + width: auto !important; + max-width: 480px !important; + box-shadow: var(--fluent-shadow-md) !important; + background: var(--fluent-bg-card) !important; + flex-direction: row !important; + margin: 0 auto !important; + display: flex !important; +} + +:is(.modal.alert-message:not(:has(.button-row)):not(:has(button)) p, .modal.spinning p) { + margin: 0 !important; +} + +#modal_overlay:has(.fluent-apply-change-preview) { + justify-content: flex-start !important; + align-items: center !important; + gap: var(--fluent-space-sm) !important; + padding-top: var(--fluent-space-sm) !important; + flex-direction: column !important; + display: flex !important; +} + +.fluent-apply-change-preview { + gap: var(--fluent-space-sm) !important; + pointer-events: auto !important; + flex-direction: column !important; + order: 1 !important; + width: min(90%, 680px) !important; + max-height: 50dvh !important; + margin: 0 auto !important; + padding: 2px !important; + display: flex !important; + overflow-y: auto !important; +} + +.fluent-apply-change-preview::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.fluent-apply-change-preview::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.fluent-apply-change-preview::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.fluent-apply-change-preview::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.fluent-apply-change-preview::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.fluent-apply-change-preview::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.fluent-apply-change-preview::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.fluent-apply-change-preview::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.fluent-apply-change-preview::-webkit-resizer { + background: none; +} + +.fluent-apply-change-preview { + animation: slideInUp var(--fluent-duration-normal) var(--fluent-easing-decelerate); +} + +.modal.alert-message.notice.spinning { + order: 2 !important; +} + +.fluent-apply-change-preview-card { + background: var(--fluent-card-bg) !important; + border-radius: var(--fluent-card-radius) !important; + box-shadow: var(--fluent-shadow-md) !important; + margin: var(--fluent-space-md) !important; + padding: var(--fluent-card-padding) !important; + gap: var(--fluent-space-sm) !important; + flex-direction: column !important; + display: flex !important; +} + +.fluent-apply-change-preview-card:not(:first-child) { + margin-top: var(--fluent-space-xs); +} + +.fluent-apply-change-preview-card-header { + font-family: var(--fluent-font-family) !important; + font-size: var(--fluent-font-size-sm) !important; + font-weight: var(--fluent-font-weight-semibold) !important; + color: var(--fluent-brand-80) !important; + letter-spacing: .5px !important; + justify-content: space-between !important; + align-items: center !important; + display: flex !important; +} + +.fluent-apply-change-preview-close { + border-radius: var(--fluent-radius-md) !important; + width: 24px !important; + height: 24px !important; + color: var(--fluent-text-secondary) !important; + cursor: pointer !important; + pointer-events: auto !important; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + background: none !important; + border: none !important; + justify-content: center !important; + align-items: center !important; + padding: 0 !important; + display: flex !important; +} + +.fluent-apply-change-preview-close:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-dismiss-20-regular: url("data:image/svg+xml,"); + width: 14px; + height: 14px; + -webkit-mask-image: var(--fluent-mask-dismiss-20-regular); + mask-image: var(--fluent-mask-dismiss-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-apply-change-preview-close:hover { + background: var(--fluent-card-border) !important; + color: var(--fluent-text) !important; +} + +.fluent-apply-change-preview-close:active { + background: color-mix(in srgb, var(--fluent-card-border) 80%, #000) !important; +} + +.fluent-apply-change-preview-card-body.uci-change-list { + max-height: 280px !important; + margin: 0 !important; +} + +:is(.modal.alert-message:has(.button-row), .modal.alert-message:has(button)) { + background: var(--fluent-modal-bg) !important; + border-radius: var(--fluent-modal-radius) !important; + width: 90% !important; + max-width: 600px !important; + padding: var(--fluent-modal-padding) !important; + box-shadow: var(--fluent-shadow-xl) !important; + flex-direction: column !important; + align-items: stretch !important; + margin: 0 auto !important; + display: flex !important; + position: relative !important; +} + +.modal.alert-message:has(.button-row):before { + top: calc(var(--fluent-modal-padding) + 4px) !important; + z-index: 11 !important; + margin: 0 !important; + position: absolute !important; + inset-inline-start: var(--fluent-modal-padding) !important; +} + +.modal.alert-message:has(button):before { + top: calc(var(--fluent-modal-padding) + 4px) !important; + z-index: 11 !important; + margin: 0 !important; + position: absolute !important; + inset-inline-start: var(--fluent-modal-padding) !important; +} + +:is(.modal.alert-message:has(.button-row) h4, .modal.alert-message:has(button) h4) { + font-size: var(--fluent-font-size-lg) !important; + font-weight: var(--fluent-font-weight-semibold) !important; + color: var(--fluent-text) !important; + margin: 0 0 var(--fluent-space-md) 0 !important; + padding-inline-start: 28px !important; +} + +:is(.modal.alert-message:has(.button-row) p, .modal.alert-message:has(button) p) { + margin: 0 0 var(--fluent-space-lg) 0 !important; + color: var(--fluent-text-secondary) !important; + font-size: var(--fluent-font-size-base) !important; + line-height: var(--fluent-line-height-relaxed) !important; +} + +:is(.modal.alert-message:has(.button-row) .button-row, .modal.alert-message:has(button) .button-row) { + margin: var(--fluent-space-lg) 0 0 0 !important; + justify-content: flex-start !important; + gap: var(--fluent-space-xs) !important; + background: none !important; + border-top: none !important; + flex-flow: row-reverse wrap !important; + padding: 0 !important; + display: flex !important; + position: static !important; +} + +.cbi-map, #view { + font-family: var(--fluent-font-family); + color: var(--fluent-text); + flex-direction: column; + min-width: 0; + max-width: 100%; + display: flex; +} + +.cbi-map > .cbi-section + .cbi-section, #view > .cbi-section + .cbi-section { + margin-top: 5px; +} + +.cbi-map h2, .cbi-map h3, #view h2, #view h3 { + color: var(--fluent-text); + font-family: var(--fluent-font-family); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-tight); + margin: 0; +} + +.cbi-map h2, #view h2 { + font-size: var(--fluent-font-size-xxl); +} + +.cbi-map h3, #view h3 { + font-size: var(--fluent-font-size-lg); +} + +.cbi-map-descr { + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-relaxed); +} + +.cbi-section-error { + background: var(--fluent-error-light); + border-color: var(--fluent-error); + box-shadow: none; +} + +.cbi-section-node-tabbed { + flex-direction: column; + display: flex; +} + +.cbi-section .cbi-title { + color: var(--fluent-text); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-md); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-tight); + margin: 0; +} + +.cbi-section .cbi-title h3 { + font-size: inherit; + font-weight: inherit; + line-height: inherit; + color: inherit; + margin: 0; +} + +.cbi-section .cbi-title h3:has(.label[data-clickable="true"]) { + justify-content: flex-start !important; + align-items: center !important; + gap: var(--fluent-space-sm) !important; + display: flex !important; +} + +.cbi-section .cbi-title h3:has(.label[data-clickable="true"]) .label[data-clickable="true"] { + cursor: pointer; + border-radius: var(--fluent-radius-sm); + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + position: relative; + width: 24px !important; + height: 24px !important; + min-width: unset !important; + color: #0000 !important; + background: none !important; + border: none !important; + order: -1 !important; + justify-content: center !important; + align-items: center !important; + margin: 0 !important; + padding: 0 !important; + font-size: 0 !important; + display: inline-flex !important; +} + +.cbi-section .cbi-title h3:has(.label[data-clickable="true"]) .label[data-clickable="true"]:hover { + background-color: var(--fluent-bg-hover) !important; +} + +.cbi-section .cbi-title h3:has(.label[data-clickable="true"]) .label[data-clickable="true"]:before { + content: ""; + border-inline-end: 1.5px solid var(--fluent-text-secondary); + border-bottom: 1.5px solid var(--fluent-text-secondary); + width: 8px; + height: 8px; + transition: transform var(--fluent-duration-normal) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + display: block; + transform: rotate(-45deg) translate(-1px, -1px); +} + +.cbi-section .cbi-title h3:has(.label[data-clickable="true"]) .label[data-clickable="true"]:hover:before { + border-color: var(--fluent-text); +} + +.cbi-section .cbi-title h3:has(.label[data-clickable="true"]) .label[data-clickable="true"][data-style="inactive"]:before { + transform: rotate(45deg) translate(0, -1px); +} + +.cbi-section-descr { + padding: 0 var(--fluent-space-lg) var(--fluent-space-md); + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-relaxed); + margin: 0; +} + +.cbi-section.fade-in { + animation: fadeIn var(--fluent-duration-normal) var(--fluent-easing-decelerate); + transform-origin: top; +} + +.cbi-value { + padding: var(--fluent-space-md) 0; + border-bottom: 1px solid var(--fluent-card-border); + display: block; +} + +.cbi-value:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +.cbi-value:first-child { + padding-top: 0; +} + +.cbi-value[data-widget="CBI.ContainerValue"] { + border-bottom: 0; + padding: 0; +} + +.cbi-value:has( > .cbi-value-title) { + gap: var(--fluent-space-xs); + flex-direction: column; + display: flex; +} + +@media (min-width: 768px) { + .cbi-value:has( > .cbi-value-title) { + align-items: start; + gap: var(--fluent-space-sm) var(--fluent-space-xl); + grid-template-columns: minmax(0, 220px) minmax(0, 1fr); + display: grid; + } +} + +@media (max-width: 767px) { + .cbi-value[data-widget="CBI.FlagValue"] { + gap: var(--fluent-space-xs) var(--fluent-space-md); + grid-template-columns: 1fr auto; + grid-template-areas: "title switch" + "desc desc"; + align-items: center; + display: grid; + } + + .cbi-value:has(.cbi-checkbox:not(td *):not(th *)) { + gap: var(--fluent-space-xs) var(--fluent-space-md); + grid-template-columns: 1fr auto; + grid-template-areas: "title switch" + "desc desc"; + align-items: center; + display: grid; + } + + .cbi-value[data-widget="CBI.FlagValue"] .cbi-value-title { + grid-area: title; + margin: 0; + } + + .cbi-value:has(.cbi-checkbox:not(td *):not(th *)) .cbi-value-title { + grid-area: title; + margin: 0; + } + + .cbi-value[data-widget="CBI.FlagValue"] .cbi-value-field:not(td):not(th) { + display: contents !important; + } + + .cbi-value:has(.cbi-checkbox:not(td *):not(th *)) .cbi-value-field:not(td):not(th) { + display: contents !important; + } + + .cbi-value[data-widget="CBI.FlagValue"] .cbi-checkbox:not(td *):not(th *) { + grid-area: switch; + justify-self: end; + align-items: center; + margin: 0; + display: flex; + } + + .cbi-value:has(.cbi-checkbox:not(td *):not(th *)) .cbi-checkbox:not(td *):not(th *) { + grid-area: switch; + justify-self: end; + align-items: center; + margin: 0; + display: flex; + } + + .cbi-value[data-widget="CBI.FlagValue"] .cbi-value-description:not(td *):not(th *) { + grid-area: desc; + margin: 0; + } + + .cbi-value:has(.cbi-checkbox:not(td *):not(th *)) .cbi-value-description:not(td *):not(th *) { + grid-area: desc; + margin: 0; + } +} + +.cbi-value .cbi-value-title { + align-self: start; +} + +.cbi-value .cbi-value-field { + min-width: 0; +} + +.cbi-value .cbi-input-text, .cbi-value .cbi-input-select, .cbi-value cbi-dropdown, .cbi-value .cbi-dropdown, .cbi-value .fluent-custom-select { + box-sizing: border-box; + width: 100%; +} + +.cbi-value-title { + color: var(--fluent-text); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-normal); + margin: 0; +} + +.cbi-value-field:not(td) { + gap: var(--fluent-space-sm); + flex-direction: column; + min-width: 0; + display: flex; +} + +.cbi-value-description { + color: var(--fluent-text-secondary); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-regular); + line-height: var(--fluent-line-height-normal); + margin: 0; +} + +.cbi-value-description abbr { + color: var(--fluent-primary); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + cursor: help; + margin-inline-start: var(--fluent-space-xs); + text-decoration: none; + display: inline-block; +} + +.cbi-optionals { + margin-top: var(--fluent-space-lg); + padding-top: var(--fluent-space-md); + border-top: 1px solid var(--fluent-card-border); +} + +.cbi-rowstyle-1, .cbi-rowstyle-2 { + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +.cbi-rowstyle-1 { + background: var(--fluent-bg); +} + +.cbi-rowstyle-2 { + background: var(--fluent-bg-page); +} + +.uci-dialog { + animation: slideInUp var(--fluent-duration-normal) var(--fluent-easing-decelerate); + background: var(--fluent-modal-bg); + border-radius: var(--fluent-radius-lg); + box-shadow: var(--fluent-shadow-xl); + color: var(--fluent-text); + border: none; +} + +.uci-dialog .cbi-section { + padding: var(--fluent-space-lg); +} + +.uci-change-list::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.uci-change-list::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.uci-change-list::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.uci-change-list::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.uci-change-list::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.uci-change-list::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.uci-change-list::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.uci-change-list::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.uci-change-list::-webkit-resizer { + background: none; +} + +.uci-change-list { + gap: var(--fluent-space-xs); + width: 100%; + max-height: calc(var(--fluent-space-xxxl) + var(--fluent-space-xxxl) + var(--fluent-space-xxxl) + var(--fluent-space-xxl) + var(--fluent-space-xl)); + padding: var(--fluent-space-sm); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); + background: var(--fluent-bg-page); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-md); + flex-direction: column; + display: flex; + overflow: auto; +} + +.uci-change-list ins, .uci-change-list del, .uci-change-list var { + padding: var(--fluent-space-sm) var(--fluent-space-md); + border-radius: var(--fluent-radius-md); + white-space: pre-wrap; + word-break: break-word; + font-style: normal; + text-decoration: none; + display: block; +} + +.uci-change-list ins { + color: var(--fluent-success); + background: var(--fluent-success-light); + border: 1px solid var(--fluent-success); +} + +.uci-change-list del { + color: var(--fluent-error); + background: var(--fluent-error-light); + border: 1px solid var(--fluent-error); +} + +.uci-change-list var { + color: var(--fluent-text-secondary); + background: var(--fluent-bg-hover); + border: 1px solid var(--fluent-border); +} + +.uci-change-list var ins, .uci-change-list var del { + white-space: inherit; + background: none; + border: 0; + border-radius: 0; + padding: 0; + display: inline; +} + +.uci-change-legend { + gap: var(--fluent-space-sm) var(--fluent-space-lg); + padding: var(--fluent-space-md) 0 0; + line-height: var(--fluent-line-height-normal); + flex-wrap: wrap; + display: flex; +} + +.uci-change-legend-label { + align-items: center; + gap: var(--fluent-space-sm); + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); + display: inline-flex; +} + +.uci-change-legend-label > ins, .uci-change-legend-label > del, .uci-change-legend-label > var { + width: calc(var(--fluent-space-sm) + var(--fluent-space-xs) + var(--fluent-space-xxs)); + height: calc(var(--fluent-space-sm) + var(--fluent-space-xs) + var(--fluent-space-xxs)); + min-width: calc(var(--fluent-space-sm) + var(--fluent-space-xs) + var(--fluent-space-xxs)); + border-radius: var(--fluent-radius-sm); + justify-content: center; + align-items: center; + padding: 0; + font-style: normal; + text-decoration: none; + display: inline-flex; +} + +.uci-change-legend-label > ins { + background: var(--fluent-success-light); + border: 1px solid var(--fluent-success); +} + +.uci-change-legend-label > del { + background: var(--fluent-error-light); + border: 1px solid var(--fluent-error); +} + +.uci-change-legend-label > var { + background: var(--fluent-bg-hover); + border: 1px solid var(--fluent-border); + color: #0000; + position: relative; +} + +.uci-change-legend-label > var ins, .uci-change-legend-label > var del { + background: none; + border: 0; + border-radius: 0; + padding: 0; + display: block; + position: absolute; + inset: 2px; +} + +.modal label > input[type="checkbox"] { + margin-right: var(--fluent-space-sm); + vertical-align: middle; +} + +.modal .label { + padding: var(--fluent-space-xxs) var(--fluent-space-sm); + border-radius: var(--fluent-radius-circular); + background: var(--fluent-info-light); + color: var(--fluent-info); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + align-items: center; + display: inline-flex; +} + +.modal .label.warning { + background: var(--fluent-warning-light); + color: var(--fluent-text); +} + +.cbi-page-actions { + z-index: var(--fluent-zindex-actions); + justify-content: flex-end; + align-items: center; + gap: var(--fluent-space-sm); + margin-top: var(--fluent-space-xl); + padding: var(--fluent-space-md) var(--fluent-space-lg); + background: color-mix(in srgb, var(--fluent-bg-card) 92%, transparent); + border-radius: var(--fluent-radius-lg); + box-shadow: var(--fluent-shadow-lg); + border: none; + flex-wrap: wrap; + display: flex; + position: sticky; + bottom: 0; +} + +body[data-sticky-action-bar="0"] .cbi-page-actions { + position: static; +} + +.cbi-page-actions > form[method="post"] { + display: inline-flex; +} + +.cbi-page-actions .cbi-button-link:first-child { + margin-right: auto; +} + +.cbi-page-actions .cbi-dropdown[open] > ul.dropdown { + margin-top: 0 !important; + margin-bottom: var(--fluent-space-xxs) !important; + transform-origin: bottom !important; + width: max-content !important; + max-width: calc(100vw - 32px) !important; + animation-name: fluent-dropdown-slide-in-up !important; + inset: auto 0 100% auto !important; +} + +html[dir="rtl"] .cbi-page-actions .cbi-dropdown[open] > ul.dropdown, body[data-fluent-dir="rtl"] .cbi-page-actions .cbi-dropdown[open] > ul.dropdown { + left: 0 !important; + right: auto !important; +} + +@supports (anchor-name: --a) { + .cbi-page-actions .cbi-dropdown[open] > ul.dropdown { + left: auto !important; + right: anchor(right) !important; + } + + html[dir="rtl"] .cbi-page-actions .cbi-dropdown[open] > ul.dropdown, body[data-fluent-dir="rtl"] .cbi-page-actions .cbi-dropdown[open] > ul.dropdown { + left: anchor(left) !important; + right: auto !important; + } +} + +:not(#modal_overlay) > .alert, :not(#modal_overlay) > .alert-message { + margin: 0 0 var(--fluent-space-lg); + padding-block: var(--fluent-space-md); + padding-inline: var(--fluent-space-lg) var(--fluent-space-lg); + border-radius: var(--fluent-radius-md); + background: var(--fluent-bg-hover); + border: 1px solid; + border-color: var(--fluent-border); + color: var(--fluent-text); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-normal); + padding-inline-start: calc(var(--fluent-space-lg) + 18px + var(--fluent-space-md)); + position: relative; +} + +:not(#modal_overlay) > .alert:before, :not(#modal_overlay) > .alert-message:before { + inset-inline-start: var(--fluent-space-lg); + top: var(--fluent-space-md); + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-info-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-info-20-regular); + mask-image: var(--fluent-mask-info-20-regular); + -webkit-user-select: none; + user-select: none; + margin-top: 2px; + display: inline-block; + position: absolute; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +:not(#modal_overlay) > .alert > div:first-of-type, :not(#modal_overlay) > .alert-message > div:first-of-type { + width: 100%; + display: flow-root; +} + +:not(#modal_overlay) > .alert > div:first-of-type .cbi-button, :not(#modal_overlay) > .alert > div:first-of-type .btn, :not(#modal_overlay) > .alert-message > div:first-of-type .cbi-button, :not(#modal_overlay) > .alert-message > div:first-of-type .btn { + height: 28px; + line-height: 1; + font-size: var(--fluent-font-size-sm); + padding: 0 var(--fluent-space-sm); + border-radius: var(--fluent-radius-md); + margin-top: var(--fluent-space-sm); + float: inline-end; +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type), :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) { + top: 8px; + display: flex; + position: absolute; + inset-inline-end: 8px; +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child:after, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child:after { + content: ""; + background: linear-gradient(45deg, transparent calc(50% - .75px), var(--fluent-text-secondary) calc(50% - .75px), var(--fluent-text-secondary) calc(50% + .75px), transparent calc(50% + .75px)),linear-gradient(-45deg, transparent calc(50% - .75px), var(--fluent-text-secondary) calc(50% - .75px), var(--fluent-text-secondary) calc(50% + .75px), transparent calc(50% + .75px)); + background-position: center; + background-repeat: no-repeat; + background-size: 8px 8px; + flex: 0 0 20px; + width: 20px; + height: 20px; + display: inline-block; +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child { + width: 28px; + height: 28px; + color: var(--fluent-text-secondary); + border-radius: var(--fluent-radius-md); + background: none; + border: none; + justify-content: center; + align-self: flex-start; + align-items: center; + padding: 0; + line-height: 1; + display: inline-flex; + margin-top: 0 !important; + margin-inline-start: auto !important; + font-size: 0 !important; +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child:hover, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child:hover { + background: var(--fluent-bg-hover); + color: var(--fluent-text); +} + +:not(#modal_overlay) > .alert > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child:active, :not(#modal_overlay) > .alert-message > div:last-of-type:not(:first-of-type) > .btn:not(.cbi-button):only-child:active { + background: var(--fluent-bg-active); +} + +:not(#modal_overlay) > .alert h4, :not(#modal_overlay) > .alert-message h4 { + margin: 0 0 var(--fluent-space-xs) 0; + color: var(--fluent-text); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-semibold); + background: none; + padding: 0; +} + +:not(#modal_overlay) > .alert p, :not(#modal_overlay) > .alert-message p { + margin: 0 0 var(--fluent-space-sm) 0; + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-relaxed); +} + +:not(#modal_overlay) > .alert p:last-child, :not(#modal_overlay) > .alert-message p:last-child { + margin-bottom: 0; +} + +:not(#modal_overlay) > .alert.success, :not(#modal_overlay) > .alert.alert-success, :not(#modal_overlay) > .alert.notice, :not(#modal_overlay) > .alert-message.success, :not(#modal_overlay) > .alert-message.alert-success, :not(#modal_overlay) > .alert-message.notice { + background: var(--fluent-success-light); + border-color: color-mix(in srgb, var(--fluent-success) 35%, transparent); +} + +:not(#modal_overlay) > .alert.success:before, :not(#modal_overlay) > .alert.alert-success:before, :not(#modal_overlay) > .alert.notice:before, :not(#modal_overlay) > .alert-message.success:before, :not(#modal_overlay) > .alert-message.alert-success:before, :not(#modal_overlay) > .alert-message.notice:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-checkmark-circle-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-checkmark-circle-20-regular); + mask-image: var(--fluent-mask-checkmark-circle-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-success) !important; +} + +:not(#modal_overlay) > .alert.warning, :not(#modal_overlay) > .alert.alert-warning, :not(#modal_overlay) > .alert-message.warning, :not(#modal_overlay) > .alert-message.alert-warning { + background: var(--fluent-warning-light); + border-color: color-mix(in srgb, var(--fluent-warning) 45%, transparent); +} + +:not(#modal_overlay) > .alert.warning:before, :not(#modal_overlay) > .alert.alert-warning:before, :not(#modal_overlay) > .alert-message.warning:before, :not(#modal_overlay) > .alert-message.alert-warning:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-warning-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-warning-20-regular); + mask-image: var(--fluent-mask-warning-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-warning) !important; +} + +:not(#modal_overlay) > .alert.error, :not(#modal_overlay) > .alert.alert-danger, :not(#modal_overlay) > .alert.danger, :not(#modal_overlay) > .alert-message.error, :not(#modal_overlay) > .alert-message.alert-danger, :not(#modal_overlay) > .alert-message.danger { + background: var(--fluent-error-light); + border-color: color-mix(in srgb, var(--fluent-error) 35%, transparent); +} + +:not(#modal_overlay) > .alert.error:before, :not(#modal_overlay) > .alert.alert-danger:before, :not(#modal_overlay) > .alert.danger:before, :not(#modal_overlay) > .alert-message.error:before, :not(#modal_overlay) > .alert-message.alert-danger:before, :not(#modal_overlay) > .alert-message.danger:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-dismiss-circle-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-dismiss-circle-20-regular); + mask-image: var(--fluent-mask-dismiss-circle-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-error) !important; +} + +:not(#modal_overlay) > .alert.info, :not(#modal_overlay) > .alert.alert-info, :not(#modal_overlay) > .alert-message.info, :not(#modal_overlay) > .alert-message.alert-info { + background: var(--fluent-info-light); + border-color: color-mix(in srgb, var(--fluent-info) 35%, transparent); +} + +:not(#modal_overlay) > .alert.info:before, :not(#modal_overlay) > .alert.alert-info:before, :not(#modal_overlay) > .alert-message.info:before, :not(#modal_overlay) > .alert-message.alert-info:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-info-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-info-20-regular); + mask-image: var(--fluent-mask-info-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-info) !important; +} + +.ifacebadge { + align-items: center; + gap: var(--fluent-space-xs); + min-height: calc(var(--fluent-input-height) - var(--fluent-space-sm)); + max-width: 100%; + padding: var(--fluent-space-xxs) var(--fluent-space-sm); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-sm); + background: var(--fluent-primary-lightest); + color: var(--fluent-text); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-tight); + box-sizing: border-box; + vertical-align: middle; + display: inline-flex; +} + +html[dir="rtl"] .ifacebadge img, body[data-fluent-dir="rtl"] .ifacebadge img { + order: -1; +} + +.ifacebadge:not(.cbi-tooltip):not(.large) > span:not(.cbi-tooltip-container) { + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 1; + min-width: 0; + overflow: hidden; +} + +.ifacebadge > em { + min-width: var(--fluent-space-lg); + color: var(--fluent-text-secondary); + flex-shrink: 0; + justify-content: center; + align-items: center; + font-style: normal; + line-height: 1; + display: inline-flex; +} + +.ifacebadge .cbi-tooltip-container { + flex-shrink: 0; + align-items: center; + display: inline-flex; +} + +.ifacebadge img { + width: var(--fluent-space-lg); + height: var(--fluent-space-lg); + object-fit: contain; + flex-shrink: 0; +} + +td > .ifacebadge, .td > .ifacebadge { + margin: var(--fluent-space-xxs) 0; +} + +.zonebadge { + align-items: center; + gap: var(--fluent-space-sm); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-radius-circular); + background: var(--fluent-card-bg); + box-shadow: none; + color: var(--fluent-zonebadge-text); + flex-wrap: wrap; + justify-content: center; + display: inline-flex; +} + +.zonebadge .ifacebadge { + background: var(--fluent-bg-page); + border-color: var(--fluent-table-border); +} + +.zonebadge > input[type="text"] { + flex: 12rem; + min-width: 0; +} + +.zonebadge > em, .zonebadge > strong { + color: inherit; + font-style: normal; + line-height: var(--fluent-line-height-normal); +} + +.zonebadge > strong { + font-weight: var(--fluent-font-weight-semibold); +} + +.zonebadge-empty { + border-style: dashed; + border-inline-start-color: var(--fluent-warning); + background: var(--fluent-bg-page); + color: var(--fluent-text-secondary); +} + +.zone-forwards { + align-items: center; + gap: var(--fluent-space-sm); + color: var(--fluent-text-secondary); + flex-wrap: wrap; + display: flex; +} + +.zone-forwards .zone-src, .zone-forwards .zone-dest { + align-items: center; + gap: var(--fluent-space-xs); + min-height: calc(var(--fluent-input-height) - var(--fluent-space-sm)); + padding: var(--fluent-space-xxs) var(--fluent-space-sm); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-sm); + background: var(--fluent-bg-page); + color: var(--fluent-text); + display: inline-flex; +} + +#syslog::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +#syslog::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +#syslog::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +#syslog::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +#syslog::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +#syslog::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +#syslog::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +#syslog::-webkit-scrollbar-corner { + background: none; + display: none; +} + +#syslog::-webkit-resizer { + background: none; +} + +#syslog { + width: 100%; + min-height: 20rem; + padding: var(--fluent-space-md); + border: 1px solid var(--fluent-border-hover); + border-radius: var(--fluent-radius-lg); + background: var(--fluent-black); + color: var(--fluent-white); + font-family: ui-monospace, Cascadia Code, SFMono-Regular, Consolas, monospace; + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-relaxed); + box-sizing: border-box; +} + +#syslog:focus { + border-color: var(--fluent-border-active); + outline: none; +} + +#diag-rc-output > pre::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +#command-rc-output > pre::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +#diag-rc-output > pre::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +#command-rc-output > pre::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +#diag-rc-output > pre::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +#command-rc-output > pre::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +#diag-rc-output > pre::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +#command-rc-output > pre::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +#diag-rc-output > pre::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +#command-rc-output > pre::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +#diag-rc-output > pre::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +#command-rc-output > pre::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +#diag-rc-output > pre::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +#command-rc-output > pre::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +#diag-rc-output > pre::-webkit-scrollbar-corner { + background: none; + display: none; +} + +#command-rc-output > pre::-webkit-scrollbar-corner { + background: none; + display: none; +} + +#diag-rc-output > pre::-webkit-resizer { + background: none; +} + +#command-rc-output > pre::-webkit-resizer { + background: none; +} + +#diag-rc-output > pre, #command-rc-output > pre { + padding: var(--fluent-space-lg); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-radius-lg); + background: var(--fluent-black); + color: var(--fluent-white); + font-family: ui-monospace, Cascadia Code, SFMono-Regular, Consolas, monospace; + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-relaxed); + white-space: pre-wrap; + word-break: break-word; + box-shadow: var(--fluent-shadow-sm); + margin: 0; + overflow: auto; +} + +.ifacebox { + background: var(--fluent-card-bg); + border-radius: var(--fluent-card-radius); + box-shadow: var(--fluent-card-shadow); + padding: var(--fluent-card-padding); + border: none; + flex-direction: column; + padding: 0; + display: inline-flex; + position: relative; + overflow: visible; +} + +.ifacebox > :first-child { + border-start-start-radius: calc(var(--fluent-card-radius) - 1px); + border-start-end-radius: calc(var(--fluent-card-radius) - 1px); +} + +.ifacebox > :last-child { + border-end-end-radius: calc(var(--fluent-card-radius) - 1px); + border-end-start-radius: calc(var(--fluent-card-radius) - 1px); +} + +.ifacebox:hover { + z-index: 1; +} + +.ifacebox > .ifacebox-body > img { + margin: 0 auto var(--fluent-space-xs); + object-fit: contain; + width: 48px; + height: 48px; + display: block; +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container { + background: none; + border: none; + min-height: 3px; + padding: 2px 8px; + position: relative; +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container > .zonebadge { + box-shadow: none; + border: none; + border-radius: 9999px; + height: 3px; + min-height: 3px; + padding: 0; +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container .cbi-tooltip { + width: max-content; + min-width: unset; + max-width: min(18rem, 80vw); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + white-space: nowrap; + font-size: var(--fluent-font-size-xs); + line-height: var(--fluent-line-height-normal); + transform: translateX(-50%) translateY(var(--fluent-space-xs)); + inset-inline: 50% auto; +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container .cbi-tooltip:before { + inset-inline-start: 50%; + transform: translateX(-50%) rotate(45deg); +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container .cbi-tooltip .ifacebadge { + align-items: center; + gap: var(--fluent-space-xs); + min-height: unset; + padding: 2px var(--fluent-space-xs); + font-size: var(--fluent-font-size-xs); + background: var(--fluent-bg-page); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-radius-sm); + margin: 2px 0; + display: inline-flex; +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container .cbi-tooltip .ifacebadge .zonebadge { + min-height: unset; + border-radius: var(--fluent-radius-circular); + height: auto; + padding: 0 4px; +} + +.ifacebox > .ifacebox-head.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + opacity: 1; + pointer-events: auto; + display: block; + transform: translateX(-50%) translateY(0); +} + +@starting-style { + .ifacebox > .ifacebox-head.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + opacity: 0; + transform: translateX(-50%) translateY(var(--fluent-space-xs)); + } +} + +.ifacebox > .ifacebox-body { + background: none; + border-bottom: none; + padding: 0; +} + +.ifacebox > .ifacebox-body .cbi-tooltip { + width: max-content; + min-width: unset; + max-width: min(20rem, 80vw); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + font-size: var(--fluent-font-size-xs); + white-space: nowrap; + transform: translateX(-50%) translateY(var(--fluent-space-xs)); + inset-inline: 50% auto; +} + +.ifacebox > .ifacebox-body .cbi-tooltip:before { + inset-inline-start: 50%; + transform: translateX(-50%) rotate(45deg); +} + +.ifacebox > .ifacebox-body .cbi-tooltip-container:hover .cbi-tooltip:not(:empty), .ifacebox > .ifacebox-body:hover .cbi-tooltip:not(:empty) { + opacity: 1; + pointer-events: auto; + display: block; + transform: translateX(-50%) translateY(0); +} + +@starting-style { + .ifacebox > .ifacebox-body .cbi-tooltip-container:hover .cbi-tooltip:not(:empty), .ifacebox > .ifacebox-body:hover .cbi-tooltip:not(:empty) { + opacity: 0; + transform: translateX(-50%) translateY(var(--fluent-space-xs)); + } +} + +.ifacebox .ifacebox { + border-radius: var(--fluent-radius-md); + width: 100%; + box-shadow: none; + border: 1px solid var(--fluent-card-border); + background: var(--fluent-bg-page); + margin: 0; + display: flex; +} + +.ifacebox .ifacebox .ifacebox-head { + padding: var(--fluent-space-xs) var(--fluent-space-md); + font-size: var(--fluent-font-size-sm); + border-bottom: 1px solid var(--fluent-card-border); + background: none; +} + +.ifacebox .ifacebox .ifacebox-head.active { + color: var(--fluent-primary); + border-bottom-color: var(--fluent-card-border); + border-inline-start: 3px solid var(--fluent-primary); + background: none; +} + +.ifacebox .ifacebox .ifacebox-body { + padding: var(--fluent-space-xs) var(--fluent-space-md); + font-size: var(--fluent-font-size-sm); +} + +.ifacebox-head { + justify-content: space-between; + align-items: center; + gap: var(--fluent-space-sm); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + border-bottom: 1px solid var(--fluent-card-border); + background: var(--fluent-bg-page); + color: var(--fluent-text); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-normal); + display: flex; +} + +.ifacebox-head.center { + text-align: center; + justify-content: center; +} + +.ifacebox-head.active { + border-bottom-color: var(--fluent-card-border); + background: var(--fluent-primary-lightest); + color: var(--fluent-primary); +} + +.ifacebox-head.active * { + color: var(--fluent-primary) !important; +} + +.ifacebox-body { + gap: var(--fluent-space-xs); + padding: var(--fluent-space-sm) var(--fluent-space-md); + color: var(--fluent-text-secondary); + text-align: start; +} + +.ifacebox-body.left { + text-align: start; + align-items: flex-start; +} + +.ifacebox-body.center { + text-align: center; + align-items: center; +} + +td[data-name="_ifacebox"], .td[data-name="_ifacebox"] { + vertical-align: middle; + min-width: 180px; + align-self: center !important; +} + +td[data-name="_ifacebox"] .ifacebox, .td[data-name="_ifacebox"] .ifacebox { + box-sizing: border-box; + width: 100%; + display: flex; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-head, .td[data-name="_ifacebox"] .ifacebox .ifacebox-head { + text-align: center; + border-bottom: 1px solid var(--fluent-card-border); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + justify-content: center; + background: none !important; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong, td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong, .td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong, .td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong { + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-normal); + background-color: color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 18%, var(--fluent-bg-page)); + border: 1px solid color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 35%, var(--fluent-card-border)); + color: color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 80%, var(--fluent-text)); + border-radius: 9999px; + justify-content: center; + align-items: center; + gap: 6px; + padding: 2px 12px; + display: inline-flex; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong:before, td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong:before, .td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong:before, .td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong:before { + content: ""; + background-color: rgb(var(--zone-color-rgb, 238, 238, 238)); + border-radius: 50%; + flex-shrink: 0; + width: 6px; + height: 6px; + display: inline-block; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body, .td[data-name="_ifacebox"] .ifacebox .ifacebox-body { + text-align: center; + box-sizing: border-box; + width: 100%; + padding: var(--fluent-space-xs) var(--fluent-space-sm) var(--fluent-space-sm); + white-space: nowrap; + display: block; + overflow-x: auto; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-scrollbar-corner { + background: none; + display: none; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-resizer { + background: none; +} + +.td[data-name="_ifacebox"] .ifacebox .ifacebox-body::-webkit-resizer { + background: none; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip-container, .td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip-container { + vertical-align: middle; + border-radius: var(--fluent-radius-sm); + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + justify-content: center; + align-items: center; + padding: 2px 4px; + display: inline-flex; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip-container:hover, .td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip-container:hover { + background-color: var(--fluent-bg-hover); +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip-container img, .td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip-container img { + vertical-align: middle; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip, .td[data-name="_ifacebox"] .ifacebox .ifacebox-body .cbi-tooltip { + display: none !important; +} + +td[data-name="_ifacebox"] .ifacebox .ifacebox-body small, .td[data-name="_ifacebox"] .ifacebox .ifacebox-body small { + margin-top: var(--fluent-space-xxs); + white-space: normal; + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-xs); + line-height: var(--fluent-line-height-tight); + display: block; +} + +[data-theme="dark"] td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong, [data-theme="dark"] td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong, [data-theme="dark"] .td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong, [data-theme="dark"] .td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong { + background-color: color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 16%, transparent); + border-color: color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 32%, transparent); + color: color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 65%, var(--fluent-text)); +} + +[data-theme="dark"] td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong:before, [data-theme="dark"] td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong:before, [data-theme="dark"] .td[data-name="_ifacebox"] .ifacebox .ifacebox-head > strong:before, [data-theme="dark"] .td[data-name="_ifacebox"] .ifacebox .ifacebox-head strong:before { + background-color: color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 75%, var(--fluent-white)); + box-shadow: 0 0 4px color-mix(in srgb, rgb(var(--zone-color-rgb, 238, 238, 238)) 40%, transparent); +} + +div[style*="display:grid;grid-template-columns:repeat"] { + align-items: stretch !important; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox { + border-radius: var(--fluent-radius-md); + flex-direction: column; + height: auto; + display: flex; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-head:first-child { + text-align: center; + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + padding: 6px var(--fluent-space-xs); + color: var(--fluent-text); + border-bottom: 1px solid var(--fluent-card-border); + background: none; + justify-content: center; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body { + padding: var(--fluent-space-xs); + text-align: center !important; + align-items: center !important; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body .cbi-tooltip-container { + color: var(--fluent-text-secondary); + font-size: 11px; + line-height: 1.35; + position: relative; + text-align: center !important; + justify-content: center !important; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body .cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + transform: translateX(-50%) translateY(0) !important; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-head.cbi-tooltip-container { + background: none; + border: none; + margin: 2px 0; + padding: 2px 8px; + position: relative; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-head.cbi-tooltip-container .zonebadge { + border-radius: 9999px; + height: 3px; + min-height: 3px; +} + +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-head.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + transform: translateX(-50%) translateY(0) !important; +} + +.network-status-table { + gap: var(--fluent-space-md); + grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); + align-items: stretch; + display: grid; +} + +.network-status-table .ifacebox { + border-radius: var(--fluent-radius-md); + flex-direction: column; + width: 100%; + margin: 0; + display: flex; +} + +.network-status-table .ifacebox .ifacebox-head { + text-align: center; + border-bottom: 1px solid var(--fluent-card-border); + padding: var(--fluent-space-xs) var(--fluent-space-sm); + background: none; + justify-content: center; +} + +.network-status-table .ifacebox .ifacebox-head > strong, .network-status-table .ifacebox .ifacebox-head strong { + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-normal); + background-color: var(--fluent-bg-page); + border: 1px solid var(--fluent-card-border); + color: var(--fluent-text-secondary); + border-radius: 9999px; + justify-content: center; + align-items: center; + gap: 6px; + padding: 2px 10px; + display: inline-flex; +} + +.network-status-table .ifacebox .ifacebox-head > strong:before, .network-status-table .ifacebox .ifacebox-head strong:before { + content: ""; + background-color: var(--fluent-text-disabled); + border-radius: 50%; + flex-shrink: 0; + width: 6px; + height: 6px; + display: inline-block; +} + +.network-status-table .ifacebox .ifacebox-head.active { + background: none; +} + +.network-status-table .ifacebox .ifacebox-head.active > strong, .network-status-table .ifacebox .ifacebox-head.active strong { + background-color: color-mix(in srgb, var(--fluent-primary) 14%, var(--fluent-bg-page)); + border-color: color-mix(in srgb, var(--fluent-primary) 35%, var(--fluent-card-border)); + color: var(--fluent-primary); +} + +.network-status-table .ifacebox .ifacebox-head.active > strong:before, .network-status-table .ifacebox .ifacebox-head.active strong:before { + background-color: var(--fluent-primary); +} + +.network-status-table .ifacebox .ifacebox-body { + padding: var(--fluent-space-sm) var(--fluent-space-md); + font-size: var(--fluent-font-size-sm); + justify-content: space-between; + gap: var(--fluent-space-sm); + flex-direction: column; + flex-grow: 1; + display: flex; +} + +.network-status-table .ifacebox .ifacebox-body > span { + color: var(--fluent-text-secondary); + word-break: break-all; + flex-direction: column; + gap: 2px; + display: flex; +} + +.network-status-table .ifacebox .ifacebox-body > span > br { + display: none; +} + +.network-status-table .ifacebox .ifacebox-body > span .nowrap { + white-space: normal; + word-break: break-word; + display: inline-block; +} + +.network-status-table .ifacebox .ifacebox-body > span .nowrap strong { + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-semibold); + margin-inline-end: var(--fluent-space-xxs); +} + +.network-status-table .ifacebox .ifacebox-body > span .cbi-tooltip-container { + cursor: help; + vertical-align: middle; + display: inline-flex; + position: relative; +} + +.network-status-table .ifacebox .ifacebox-body > div { + gap: var(--fluent-space-xs); + flex-direction: column; + display: flex; +} + +.network-status-table .ifacebox .ifacebox-body > div .ifacebadge { + width: 100%; + min-height: unset; + padding: var(--fluent-space-xs) var(--fluent-space-sm); + background: var(--fluent-bg-page); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-radius-sm); + align-items: flex-start; + display: flex; +} + +.network-status-table .ifacebox .ifacebox-body > div .ifacebadge > img { + margin-top: 2px; +} + +.network-status-table .ifacebox .ifacebox-body > div .ifacebadge > span { + white-space: normal; + font-size: var(--fluent-font-size-xs); + line-height: var(--fluent-line-height-normal); + flex-direction: column; + gap: 2px; + display: flex; + overflow: visible; +} + +.network-status-table .ifacebox .ifacebox-body > div .ifacebadge > span > br { + display: none; +} + +.network-status-table .ifacebox .ifacebox-body > div .ifacebadge > span .nowrap { + white-space: normal; + word-break: break-word; +} + +.network-status-table .ifacebox .ifacebox-body > div .ifacebadge > span .nowrap strong { + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-semibold); +} + +#fluent-global-tooltip, .cbi-tooltip.ifacebadge.large { + align-items: flex-start; + gap: var(--fluent-space-sm); + min-width: 14rem; + max-width: min(24rem, 90vw); + padding: var(--fluent-space-sm) var(--fluent-space-md); + background: var(--fluent-card-bg); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-radius-md); + box-shadow: var(--fluent-shadow-md); + color: var(--fluent-text); + font-size: var(--fluent-font-size-xs); + font-weight: var(--fluent-font-weight-normal); + line-height: var(--fluent-line-height-normal); + white-space: normal; + opacity: 0; + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); + flex-direction: row; + overflow: visible; + display: flex !important; + transform: translateY(0) !important; +} + +#fluent-global-tooltip:before, .cbi-tooltip.ifacebadge.large:before { + background-color: var(--fluent-card-bg); + border-inline-start: 1px solid var(--fluent-card-border); + border-top: 1px solid var(--fluent-card-border); + left: var(--arrow-left, 50%) !important; + transform: translateX(-50%) rotate(45deg) !important; +} + +#fluent-global-tooltip > img, .cbi-tooltip.ifacebadge.large > img { + object-fit: contain; + flex-shrink: 0; + width: 24px; + height: 24px; + margin-top: 2px; +} + +#fluent-global-tooltip > .left, #fluent-global-tooltip > span, .cbi-tooltip.ifacebadge.large > .left, .cbi-tooltip.ifacebadge.large > span { + white-space: normal; + min-width: 0; + color: var(--fluent-text-secondary); + flex-direction: column; + flex: auto; + gap: 2px; + display: flex; + overflow: visible; +} + +#fluent-global-tooltip > .left > br, #fluent-global-tooltip > span > br, .cbi-tooltip.ifacebadge.large > .left > br, .cbi-tooltip.ifacebadge.large > span > br { + display: none; +} + +#fluent-global-tooltip > .left .nowrap, #fluent-global-tooltip > span .nowrap, .cbi-tooltip.ifacebadge.large > .left .nowrap, .cbi-tooltip.ifacebadge.large > span .nowrap { + white-space: normal; + word-break: break-word; +} + +#fluent-global-tooltip > .left .nowrap strong, #fluent-global-tooltip > span .nowrap strong, .cbi-tooltip.ifacebadge.large > .left .nowrap strong, .cbi-tooltip.ifacebadge.large > span .nowrap strong { + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-semibold); +} + +.cbi-input-invalid { + border-color: var(--fluent-error); + border-bottom-color: var(--fluent-error); + box-shadow: inset 0 -1px 0 var(--fluent-error); +} + +input[type="text"].cbi-input-invalid, input[type="password"].cbi-input-invalid { + background: var(--fluent-input-bg); + color: var(--fluent-text); +} + +input[type="text"].cbi-input-invalid:focus, input[type="password"].cbi-input-invalid:focus { + box-shadow: inset 0 -2px 0 var(--fluent-error),0 0 0 1px var(--fluent-error-light); + outline: none; +} + +select.cbi-input-invalid { + background-color: var(--fluent-input-bg); + color: var(--fluent-text); + border-color: var(--fluent-error); + border-bottom-color: var(--fluent-error); +} + +.cbi-input-file { + width: 100%; + max-width: 100%; + padding: var(--fluent-space-xs); + color: var(--fluent-text); + background: var(--fluent-input-bg); + border: 1px solid var(--fluent-input-border); + border-radius: var(--fluent-radius-md); + box-sizing: border-box; + border-bottom-width: 2px; +} + +.cbi-input-file::file-selector-button { + margin-right: var(--fluent-space-sm); +} + +#cbi-upload { + align-items: center; + gap: var(--fluent-space-sm); + padding: var(--fluent-space-md); + background: var(--fluent-card-bg); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-card-radius); + box-shadow: var(--fluent-card-shadow); + flex-wrap: wrap; + display: flex; +} + +.fluent-bg-manager { + gap: var(--fluent-space-md); + padding: var(--fluent-space-lg); + background: var(--fluent-bg-card); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-lg); + box-shadow: var(--fluent-shadow-md); + flex-direction: column; + display: flex; +} + +.fluent-bg-status, .fluent-bg-hint { + margin: 0; +} + +.fluent-bg-hint { + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); +} + +.fluent-bg-actions { + gap: var(--fluent-space-sm); + flex-wrap: wrap; + display: flex; +} + +.fluent-bg-list { + gap: var(--fluent-space-md); + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + display: grid; +} + +.fluent-bg-item { + gap: var(--fluent-space-sm); + min-width: 0; + padding: var(--fluent-space-sm); + background: var(--fluent-bg-page); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-lg); + flex-direction: column; + display: flex; +} + +.fluent-bg-preview { + aspect-ratio: 16 / 9; + border-radius: var(--fluent-radius-md); + background-color: var(--fluent-bg-hover); + background-position: center; + background-repeat: no-repeat; + background-size: cover; + width: 100%; + overflow: hidden; +} + +.fluent-bg-preview-video { + object-fit: cover; + display: block; +} + +.fluent-bg-meta { + gap: var(--fluent-space-xxs); + flex-direction: column; + min-width: 0; + display: flex; +} + +.fluent-bg-name { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.fluent-bg-size { + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); +} + +.fluent-bg-item-actions { + justify-content: flex-end; + display: flex; +} + +.fluent-bg-empty { + gap: var(--fluent-space-xxs); + padding: var(--fluent-space-xl); + color: var(--fluent-text-secondary); + background: var(--fluent-bg-page); + border: 1px dashed var(--fluent-border); + border-radius: var(--fluent-radius-lg); + text-align: center; + flex-direction: column; + display: flex; +} + +#cbi-upload.cbi-fileupload { + padding: var(--fluent-space-lg); + border-radius: var(--fluent-radius-lg); + align-items: stretch; +} + +#cbi-upload.cbi-fileupload > p { + width: 100%; + color: var(--fluent-text-secondary); + margin: 0; +} + +#cbi-upload.cbi-fileupload > .cbi-input-file { + flex: 240px; + min-width: 0; +} + +#cbi-upload.cbi-fileupload > .btn, #cbi-upload.cbi-fileupload > .cbi-button { + min-width: 120px; +} + +.cbi-checkbox { + align-items: center; + gap: var(--fluent-space-sm); + min-height: var(--fluent-checkbox-size); + color: var(--fluent-text); + display: inline-flex; +} + +.cbi-image-button { + gap: var(--fluent-space-xs); + min-width: var(--fluent-btn-height); + padding: 0 var(--fluent-space-sm); +} + +.cbi-tab-descr { + margin-top: var(--fluent-space-xs); + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); +} + +div[depends], [data-depends] { + min-width: 0; + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +div[depends][hidden], div[depends][style*="display:none"], [data-depends][hidden], [data-depends][style*="display:none"] { + opacity: 0; +} + +div.tr[data-title] { + gap: var(--fluent-space-xs); + padding: var(--fluent-space-sm) 0; + flex-direction: column; +} + +div.tr[data-title]:before { + content: attr(data-title); + padding: 0 var(--fluent-space-lg); + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-normal); +} + +div.td[data-title] { + gap: var(--fluent-space-sm); + grid-template-columns: minmax(7rem, 40%) minmax(0, 1fr); + align-items: start; + display: grid; +} + +div.td[data-title]:before { + content: attr(data-title); + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-normal); +} + +div.td[data-type="button"] { + justify-content: flex-start; + align-items: center; + gap: var(--fluent-space-xs); + flex-wrap: wrap; + display: flex; +} + +div.td[data-type="button"]:before { + width: 100%; +} + +div.td[data-type="fvalue"] { + gap: var(--fluent-space-xxs); + color: var(--fluent-text-secondary); + flex-direction: column; + display: flex; +} + +@media (min-width: 768px) { + div.tr[data-title] { + flex-direction: row; + gap: 0; + padding: 0; + } + + div.tr[data-title]:before, div.td[data-title]:before { + content: none; + } + + div.td[data-title], div.td[data-type="button"], div.td[data-type="fvalue"] { + display: block; + } +} + +ul.tabs, ul.cbi-tabmenu { + box-sizing: border-box; + align-items: center; + gap: var(--fluent-space-sm, 8px); + width: 100%; + max-width: 100%; + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + line-height: var(--fluent-line-height-normal); + -webkit-overflow-scrolling: touch; + scrollbar-width: thin; + scrollbar-color: var(--fluent-scrollbar-thumb-horizontal-bg, var(--fluent-scrollbar-thumb-bg)) #0000; + margin: 0; + padding: 0; + list-style: none; + display: flex; + position: relative; + overflow: auto hidden; + border-bottom: none !important; +} + +ul.tabs::-webkit-scrollbar { + height: 3px; +} + +ul.cbi-tabmenu::-webkit-scrollbar { + height: 3px; +} + +ul.tabs::-webkit-scrollbar-track { + background: none; +} + +ul.cbi-tabmenu::-webkit-scrollbar-track { + background: none; +} + +ul.tabs::-webkit-scrollbar-thumb { + background-color: var(--fluent-scrollbar-thumb-horizontal-bg, var(--fluent-scrollbar-thumb-bg)); + border-radius: var(--fluent-radius-md); +} + +ul.cbi-tabmenu::-webkit-scrollbar-thumb { + background-color: var(--fluent-scrollbar-thumb-horizontal-bg, var(--fluent-scrollbar-thumb-bg)); + border-radius: var(--fluent-radius-md); +} + +ul.tabs::-webkit-scrollbar-thumb:hover { + background-color: var(--fluent-scrollbar-thumb-horizontal-bg-hover, var(--fluent-scrollbar-thumb-bg-hover)); +} + +ul.cbi-tabmenu::-webkit-scrollbar-thumb:hover { + background-color: var(--fluent-scrollbar-thumb-horizontal-bg-hover, var(--fluent-scrollbar-thumb-bg-hover)); +} + +ul.tabs .fluent-tab-slider, ul.cbi-tabmenu .fluent-tab-slider { + height: var(--fluent-tab-indicator-height, 3px); + background-color: var(--fluent-primary); + border-radius: calc(var(--fluent-tab-indicator-height, 3px)/2); + transition: inset-inline-start var(--fluent-duration-normal) var(--fluent-easing-standard),width var(--fluent-duration-normal) var(--fluent-easing-standard),transform var(--fluent-duration-normal) var(--fluent-easing-standard); + transform-origin: center; + pointer-events: none; + z-index: 1; + position: absolute; + bottom: 0; +} + +body[data-tab-animation="0"] ul.tabs .fluent-tab-slider, body[data-tab-animation="0"] ul.cbi-tabmenu .fluent-tab-slider, body[data-reduce-motion="true"] ul.tabs .fluent-tab-slider, body[data-reduce-motion="true"] ul.cbi-tabmenu .fluent-tab-slider { + transition: none !important; +} + +ul.tabs > li, ul.cbi-tabmenu > li { + box-sizing: border-box; + flex-shrink: 0; + align-items: center; + margin: 0; + padding: 0; + list-style: none; + display: flex; + position: relative; +} + +ul.tabs > li > a, ul.cbi-tabmenu > li > a { + box-sizing: border-box; + min-height: var(--fluent-tab-height, 44px); + padding: var(--fluent-space-xs, 4px) var(--fluent-space-lg, 16px); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + line-height: var(--fluent-line-height-normal); + color: var(--fluent-text-secondary); + cursor: pointer; + -webkit-user-select: none; + user-select: none; + white-space: nowrap; + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard),font-weight var(--fluent-duration-fast) var(--fluent-easing-standard); + border: none; + justify-content: center; + align-items: center; + text-decoration: none; + display: flex; + position: relative; + background-color: #0000 !important; +} + +ul.tabs > li > a:after, ul.cbi-tabmenu > li > a:after { + content: ""; + inset-inline-start: var(--fluent-space-lg, 16px); + inset-inline-end: var(--fluent-space-lg, 16px); + height: var(--fluent-tab-indicator-height, 3px); + border-radius: calc(var(--fluent-tab-indicator-height, 3px)/2); + transform-origin: center; + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard),background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + background-color: #0000; + position: absolute; + bottom: 0; + transform: scaleX(0); +} + +ul.tabs > li > a:hover, ul.cbi-tabmenu > li > a:hover { + color: var(--fluent-text); + background-color: #0000 !important; +} + +ul.tabs > li > a:hover:after, ul.cbi-tabmenu > li > a:hover:after { + background-color: var(--fluent-border-hover); + transform: scaleX(1); +} + +ul.tabs > li > a:focus-visible, ul.cbi-tabmenu > li > a:focus-visible { + outline: 2px solid var(--fluent-focus-color); + outline-offset: -2px; +} + +ul.tabs > li.cbi-tab > a, ul.tabs > li.active > a, ul.cbi-tabmenu > li.cbi-tab > a, ul.cbi-tabmenu > li.active > a { + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-semibold); + background-color: #0000 !important; +} + +ul.tabs > li.cbi-tab > a:after, ul.tabs > li.active > a:after, ul.cbi-tabmenu > li.cbi-tab > a:after, ul.cbi-tabmenu > li.active > a:after { + display: none !important; +} + +ul.tabs > li.cbi-tab > a:hover, ul.tabs > li.active > a:hover, ul.cbi-tabmenu > li.cbi-tab > a:hover, ul.cbi-tabmenu > li.active > a:hover { + color: var(--fluent-text); + background-color: #0000 !important; +} + +ul.tabs > li.cbi-tab > a:hover:after, ul.tabs > li.active > a:hover:after, ul.cbi-tabmenu > li.cbi-tab > a:hover:after, ul.cbi-tabmenu > li.active > a:hover:after { + display: none !important; +} + +ul.tabs > li.cbi-tab-disabled > a, ul.cbi-tabmenu > li.cbi-tab-disabled > a { + color: var(--fluent-text-secondary); + cursor: pointer; +} + +ul.tabs > li.cbi-tab-disabled > a:hover, ul.cbi-tabmenu > li.cbi-tab-disabled > a:hover { + color: var(--fluent-text); + background-color: #0000 !important; +} + +ul.tabs > li.disabled > a, ul.tabs > li[disabled] > a, ul.cbi-tabmenu > li.disabled > a, ul.cbi-tabmenu > li[disabled] > a { + color: var(--fluent-text-disabled); + cursor: not-allowed; + pointer-events: none; +} + +ul.tabs > li.disabled > a:after, ul.tabs > li[disabled] > a:after, ul.cbi-tabmenu > li.disabled > a:after, ul.cbi-tabmenu > li[disabled] > a:after { + display: none !important; +} + +.cbi-section > .cbi-tabmenu + .cbi-section-node { + padding-top: 0; +} + +#tabmenu, .tabmenu { + box-sizing: border-box; + width: 100%; + max-width: 100%; + margin: 0; + padding: 0; +} + +[data-initialized="true"] > [data-tab][data-tab-title][data-tab-title] { + padding-inline-start: var(--fluent-space-sm); +} + +[data-initialized="true"] > [data-tab][data-tab-title][data-tab-active="true"] { + opacity: 1; + height: auto; + padding-top: var(--fluent-space-lg); + display: block; + overflow: visible; +} + +[data-initialized="true"] > [data-tab][data-tab-title]:not([data-tab-active="true"]) { + display: none !important; +} + +[data-initialized="true"] > [data-tab][data-tab-title][data-tab-active="true"]:has(.cbi-section-node[data-initialized="true"] > [data-tab][data-tab-title][data-tab-active="true"]) { + padding-top: 0; +} + +.cbi-tabcontainer { + padding: var(--fluent-space-lg) 0; +} + +input[type="checkbox"] { + appearance: none !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-bg) !important; + cursor: pointer !important; + vertical-align: middle !important; + margin: 0 !important; + margin-bottom: 0 !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + border-radius: 10px !important; + outline: none !important; + flex-shrink: 0 !important; + width: 40px !important; + min-width: 40px !important; + height: 20px !important; + min-height: 20px !important; + margin-inline-end: 0 !important; + padding: 0 !important; + transition: background-color .1s cubic-bezier(.33, 0, .67, 1), border-color .1s cubic-bezier(.33, 0, .67, 1) !important; + position: relative !important; +} + +input[type="checkbox"]:before { + content: "" !important; + top: 3px !important; + background-color: var(--fluent-text-secondary) !important; + box-sizing: border-box !important; + background-image: none !important; + border-radius: 50% !important; + width: 12px !important; + min-width: 12px !important; + height: 12px !important; + min-height: 12px !important; + transition: transform .1s cubic-bezier(.33, 0, .67, 1), background-color .1s cubic-bezier(.33, 0, .67, 1) !important; + display: block !important; + position: absolute !important; + inset-inline-start: 3px !important; + box-shadow: 0 0 2px #0000003d, 0 1px 2px #00000047 !important; +} + +input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; +} + +input[type="checkbox"]:checked:before { + transform: translateX(20px) !important; +} + +html[dir="rtl"] input[type="checkbox"]:checked:before, body[data-fluent-dir="rtl"] input[type="checkbox"]:checked:before { + transform: scaleX(-1) translateX(20px) !important; +} + +input[type="checkbox"]:checked:before { + background-color: var(--fluent-white) !important; + background-image: none !important; +} + +input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-text) !important; + background-image: none !important; +} + +input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: none !important; +} + +input[type="checkbox"]:focus, input[type="checkbox"]:focus-visible { + outline: 2px solid var(--fluent-focus-color) !important; + outline-offset: 2px !important; + outline-style: solid !important; +} + +input[type="checkbox"]:active:not(:disabled):before { + background-image: none !important; + width: 14px !important; +} + +input[type="checkbox"]:checked:active:not(:disabled):before { + transform: translateX(18px) !important; +} + +html[dir="rtl"] input[type="checkbox"]:checked:active:not(:disabled):before, body[data-fluent-dir="rtl"] input[type="checkbox"]:checked:active:not(:disabled):before { + transform: scaleX(-1) translateX(18px) !important; +} + +input[type="checkbox"]:checked:active:not(:disabled):before { + background-image: none !important; + width: 14px !important; +} + +input[type="checkbox"]:disabled { + cursor: not-allowed !important; + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + background-image: none !important; +} + +input[type="checkbox"]:disabled:before { + background-color: var(--fluent-border-hover) !important; + background-image: none !important; +} + +input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: none !important; +} + +input[type="checkbox"]:disabled:checked:before { + background-color: var(--fluent-bg-hover) !important; + background-image: none !important; + transform: translateX(20px) !important; +} + +html[dir="rtl"] input[type="checkbox"]:disabled:checked:before, body[data-fluent-dir="rtl"] input[type="checkbox"]:disabled:checked:before { + transform: scaleX(-1) translateX(20px) !important; +} + +.table input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +.table input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +.table input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +.table input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +.table input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +.table input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +.table input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +.table input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +.cbi-section-table input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +.cbi-section-table input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +.cbi-section-table input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +.cbi-section-table input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +.cbi-section-table input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +.cbi-section-table input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +.cbi-section-table input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +.cbi-section-table input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +.cbi-dropdown input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +.cbi-dropdown input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +.cbi-dropdown input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +.cbi-dropdown input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +.cbi-dropdown input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +.cbi-dropdown input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +.cbi-dropdown input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +.cbi-dropdown input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +td input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +td input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +td input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +td input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +td input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +td input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +td input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +td input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +th input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +th input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +th input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +th input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +th input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +th input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +th input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +th input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +.tr .td input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +.tr .td input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +.tr .td input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +.tr .td input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +.tr .td input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +.tr .td input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +.tr .td input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +.tr .td input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +.tr .th input[type="checkbox"] { + border-radius: var(--fluent-radius-md) !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + margin: 0 auto !important; + margin-inline-end: 3px !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: auto !important; + border-style: solid !important; + border-width: 1px !important; + width: 20px !important; + min-width: 20px !important; + height: 20px !important; + min-height: 20px !important; + margin-bottom: 0 !important; +} + +.tr .th input[type="checkbox"]:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; +} + +.tr .th input[type="checkbox"]:checked { + background-color: var(--fluent-primary) !important; + border-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +.tr .th input[type="checkbox"]:checked:before { + content: none !important; + width: 0 !important; + height: 0 !important; + display: none !important; + transform: none !important; +} + +.tr .th input[type="checkbox"]:hover:not(:disabled) { + background-color: var(--fluent-bg-hover) !important; + border-color: var(--fluent-checkbox-hover-border) !important; + background-image: none !important; +} + +.tr .th input[type="checkbox"]:hover:not(:disabled):checked { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +.tr .th input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + opacity: .6 !important; + cursor: not-allowed !important; + background-image: none !important; +} + +.tr .th input[type="checkbox"]:disabled:checked { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0iIzZiNmI2YiIgZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=) !important; +} + +label:has(input[type="radio"]) { + color: var(--fluent-text-secondary); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + line-height: var(--fluent-line-height-normal); + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard); + -webkit-user-select: none; + user-select: none; + align-items: center !important; + gap: var(--fluent-space-sm) !important; + cursor: pointer !important; + display: inline-flex !important; +} + +label:has(input[type="radio"]):has(input[type="radio"]:checked) { + color: var(--fluent-text) !important; +} + +label:has(input[type="radio"]):hover:not(:has(input[type="radio"]:disabled)) { + color: var(--fluent-text); +} + +label:has(input[type="radio"]):hover:not(:has(input[type="radio"]:disabled)) input[type="radio"] { + border-color: var(--fluent-checkbox-hover-border); +} + +label:has(input[type="radio"]):hover:not(:has(input[type="radio"]:disabled)) input[type="radio"]:checked { + border-color: var(--fluent-primary-hover); +} + +label:has(input[type="radio"]):hover:not(:has(input[type="radio"]:disabled)) input[type="radio"]:checked:before { + background: var(--fluent-primary-hover); + transform: translate(-50%, -50%) scale(1.1); +} + +input[type="radio"] { + appearance: none; + border: 1.5px solid var(--fluent-checkbox-border); + border-radius: var(--fluent-radius-round); + background: var(--fluent-checkbox-bg); + cursor: pointer; + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + flex-shrink: 0; + position: relative; + width: 16px !important; + height: 16px !important; + block-size: 16px !important; + inline-size: 16px !important; +} + +input[type="radio"]:before { + content: ""; + background: var(--fluent-checkbox-checked-bg); + border-radius: var(--fluent-radius-round); + width: 8px; + height: 8px; + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard),background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) scale(0); +} + +input[type="radio"]:checked { + border-color: var(--fluent-checkbox-checked-bg); +} + +input[type="radio"]:checked:before { + transform: translate(-50%, -50%) scale(1); +} + +input[type="radio"]:hover { + border-color: var(--fluent-checkbox-hover-border); +} + +input[type="radio"]:hover:checked { + border-color: var(--fluent-primary-hover); +} + +input[type="radio"]:hover:checked:before { + background: var(--fluent-primary-hover); +} + +input[type="radio"]:focus-visible { + outline: 2px solid var(--fluent-focus-color); + outline-offset: 2px; +} + +input[type="radio"]:disabled { + opacity: .4; + cursor: not-allowed; +} + +html::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +html::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +html::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +html::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +html::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +html::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +html::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +html::-webkit-scrollbar-corner { + background: none; + display: none; +} + +html::-webkit-resizer { + background: none; +} + +.cbi-section::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.cbi-section-node::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.table-wrapper::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.modal::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.main-content::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.sidebar::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +pre::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +code::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.cbi-section::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.cbi-section-node::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.table-wrapper::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.modal::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.main-content::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.sidebar::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +pre::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +code::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.cbi-section::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.cbi-section-node::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.table-wrapper::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.modal::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.main-content::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.sidebar::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +pre::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +code::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.cbi-section::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.cbi-section-node::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.table-wrapper::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.modal::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.main-content::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.sidebar::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +pre::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +code::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.cbi-section::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.cbi-section-node::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.table-wrapper::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.modal::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.main-content::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.sidebar::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +pre::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +code::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.cbi-section::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.cbi-section-node::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.table-wrapper::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.modal::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.main-content::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.sidebar::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +pre::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +code::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.cbi-section::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.cbi-section-node::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.table-wrapper::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.modal::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.main-content::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.sidebar::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +pre::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +code::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.cbi-section::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.cbi-section-node::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.table-wrapper::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.modal::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.main-content::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.sidebar::-webkit-scrollbar-corner { + background: none; + display: none; +} + +pre::-webkit-scrollbar-corner { + background: none; + display: none; +} + +code::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.cbi-section::-webkit-resizer { + background: none; +} + +.cbi-section-node::-webkit-resizer { + background: none; +} + +.table-wrapper::-webkit-resizer { + background: none; +} + +.modal::-webkit-resizer { + background: none; +} + +.main-content::-webkit-resizer { + background: none; +} + +.sidebar::-webkit-resizer { + background: none; +} + +pre::-webkit-resizer { + background: none; +} + +code::-webkit-resizer { + background: none; +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.cbi-dropdown ul.dropdown::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.fluent-custom-select ul.dropdown::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.cbi-dropdown ul.dropdown::-webkit-resizer { + background: none; +} + +.fluent-custom-select ul.dropdown::-webkit-resizer { + background: none; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--fluent-scrollbar-thumb-bg) var(--fluent-scrollbar-track-bg); +} + +.sidebar::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.cbi-section-node::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.table-wrapper::-webkit-scrollbar { + width: 4px; + height: 4px; +} + +.cbi-progressbar { + box-sizing: border-box; + isolation: isolate; + width: 100%; + height: 24px; + position: relative; + box-shadow: none !important; + background: none !important; + border: none !important; +} + +.cbi-progressbar:before { + content: ""; + width: 100%; + height: var(--fluent-progress-height, 4px); + background: var(--fluent-progress-bg); + border-radius: var(--fluent-radius-circular, 10000px); + position: absolute; + bottom: 0; + left: 0; +} + +.cbi-progressbar > div { + height: var(--fluent-progress-height, 4px); + background: var(--fluent-progress-bar-bg); + border-radius: var(--fluent-radius-circular, 10000px); + transition: width var(--fluent-duration-normal) var(--fluent-easing-decelerate); + position: absolute; + bottom: 0; + left: 0; + overflow: hidden; +} + +html[dir="rtl"] .cbi-progressbar > div, body[data-fluent-dir="rtl"] .cbi-progressbar > div { + left: auto; + right: 0; +} + +.cbi-progressbar:after { + content: attr(title); + color: var(--fluent-progressbar-font-color); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-sm, 12px); + font-weight: var(--fluent-font-weight-regular, 400); + line-height: var(--fluent-line-height-normal, 1.43); + white-space: nowrap; + text-overflow: ellipsis; + pointer-events: none; + position: absolute; + top: 0; + left: 0; + right: 0; + overflow: hidden; +} + +html[dir="rtl"] .cbi-progressbar:after, body[data-fluent-dir="rtl"] .cbi-progressbar:after { + text-align: right; + direction: ltr; +} + +body[data-progressbar-text="bottom-start"] .cbi-progressbar:before, body[data-progressbar-text="bottom-end"] .cbi-progressbar:before, body[data-progressbar-text="bottom-start"] .cbi-progressbar > div, body[data-progressbar-text="bottom-end"] .cbi-progressbar > div { + top: 0; + bottom: auto; +} + +body[data-progressbar-text="bottom-start"] .cbi-progressbar:after, body[data-progressbar-text="bottom-end"] .cbi-progressbar:after { + top: 8px; +} + +body[data-progressbar-text="top-end"] .cbi-progressbar:after, body[data-progressbar-text="bottom-end"] .cbi-progressbar:after { + text-align: end; +} + +html[dir="rtl"] body[data-progressbar-text="top-end"] .cbi-progressbar:after, body[data-fluent-dir="rtl"] body[data-progressbar-text="top-end"] .cbi-progressbar:after, html[dir="rtl"] body[data-progressbar-text="bottom-end"] .cbi-progressbar:after, body[data-fluent-dir="rtl"] body[data-progressbar-text="bottom-end"] .cbi-progressbar:after { + text-align: left; + direction: ltr; +} + +@keyframes progressShimmer { + 0% { + transform: translateX(-100%); + } + + 100% { + transform: translateX(100%); + } +} + +.cbi-progressbar-success > div { + background: var(--fluent-success); +} + +.cbi-progressbar-warning > div { + background: var(--fluent-warning); +} + +.cbi-progressbar-error > div { + background: var(--fluent-error); +} + +@keyframes fluent-dropdown-slide-in { + from { + opacity: 0; + transform: translateY(-8px) scaleY(.95); + } + + to { + opacity: 1; + transform: translateY(0) scaleY(1); + } +} + +@keyframes fluent-dropdown-slide-in-up { + from { + opacity: 0; + transform: translateY(8px) scaleY(.95); + } + + to { + opacity: 1; + transform: translateY(0) scaleY(1); + } +} + +cbi-dropdown, .cbi-dropdown, .fluent-custom-select { + box-sizing: border-box; + vertical-align: middle; + justify-content: space-between; + align-items: center; + display: inline-flex; + position: relative; +} + +html[dir="rtl"] cbi-dropdown, body[data-fluent-dir="rtl"] cbi-dropdown, html[dir="rtl"] .cbi-dropdown, body[data-fluent-dir="rtl"] .cbi-dropdown, html[dir="rtl"] .fluent-custom-select, body[data-fluent-dir="rtl"] .fluent-custom-select { + direction: rtl; +} + +cbi-dropdown, .cbi-dropdown, .fluent-custom-select { + padding: 0 var(--fluent-space-xs); + gap: var(--fluent-space-xxs); + cursor: pointer; + outline-style: none; + min-width: 120px; + line-height: 20px; + position: relative; +} + +cbi-dropdown:hover:not([disabled]):not(:focus):not(:focus-within):not(.focus):not([open]), .cbi-dropdown:hover:not([disabled]):not(:focus):not(:focus-within):not(.focus):not([open]), .fluent-custom-select:hover:not([disabled]):not(:focus):not(:focus-within):not(.focus):not([open]) { + border-color: var(--fluent-input-border-hover); + border-bottom-color: var(--fluent-input-border-bottom-hover); +} + +cbi-dropdown:focus, cbi-dropdown:focus-within, cbi-dropdown.focus, cbi-dropdown[open], .cbi-dropdown:focus, .cbi-dropdown:focus-within, .cbi-dropdown.focus, .cbi-dropdown[open], .fluent-custom-select:focus, .fluent-custom-select:focus-within, .fluent-custom-select.focus, .fluent-custom-select[open] { + border-bottom-color: var(--fluent-input-border-bottom-focus); + outline: none; +} + +cbi-dropdown:focus:after, cbi-dropdown:focus-within:after, cbi-dropdown.focus:after, cbi-dropdown[open]:after, .cbi-dropdown:focus:after, .cbi-dropdown:focus-within:after, .cbi-dropdown.focus:after, .cbi-dropdown[open]:after, .fluent-custom-select:focus:after, .fluent-custom-select:focus-within:after, .fluent-custom-select.focus:after, .fluent-custom-select[open]:after { + transform: scaleX(1); +} + +cbi-dropdown:after, .cbi-dropdown:after, .fluent-custom-select:after { + box-sizing: border-box; + content: ""; + inset-inline-start: 1px; + bottom: -1px; + background: var(--fluent-input-border-bottom-focus); + height: 2px; + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard); + pointer-events: none; + border-end-end-radius: var(--fluent-radius-sm); + border-end-start-radius: var(--fluent-radius-sm); + position: absolute; + inset-inline-end: 1px; + transform: scaleX(0); +} + +cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open] { + z-index: var(--fluent-zindex-dropdown); + anchor-name: --fluent-cbi-dropdown-active !important; +} + +cbi-dropdown[disabled], .cbi-dropdown[disabled], .fluent-custom-select[disabled] { + cursor: not-allowed; + background: var(--fluent-input-bg-disabled); + border-color: var(--fluent-border); + border-bottom-color: var(--fluent-border); + color: var(--fluent-text-disabled); + opacity: 1; +} + +cbi-dropdown[disabled]:after, .cbi-dropdown[disabled]:after, .fluent-custom-select[disabled]:after { + content: none; +} + +cbi-dropdown > div, cbi-dropdown > div[tabindex="0"], .cbi-dropdown > div, .cbi-dropdown > div[tabindex="0"], .fluent-custom-select > div, .fluent-custom-select > div[tabindex="0"] { + pointer-events: none !important; + width: 0 !important; + height: 0 !important; + position: absolute !important; + overflow: hidden !important; +} + +cbi-dropdown > ul.preview, cbi-dropdown > ul:not(.dropdown), .cbi-dropdown > ul.preview, .cbi-dropdown > ul:not(.dropdown), .fluent-custom-select > ul.preview, .fluent-custom-select > ul:not(.dropdown) { + text-overflow: ellipsis; + white-space: nowrap; + align-items: center; + gap: var(--fluent-space-xxs); + pointer-events: none; + height: 100%; + min-height: unset; + flex-wrap: nowrap; + flex: 1; + margin: 0; + padding-block: 0; + padding-inline: 3.5px 0; + list-style: none; + display: flex; + overflow: hidden; + -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + text-overflow: ellipsis; + white-space: nowrap; + align-items: center; + gap: var(--fluent-space-xxs); + pointer-events: none; + height: 100%; + min-height: unset; + flex-wrap: nowrap; + flex: 1; + margin: 0; + padding-block: 0; + padding-inline: 3.5px 0; + list-style: none; + display: flex; + overflow: hidden; + -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + text-overflow: ellipsis; + white-space: nowrap; + align-items: center; + gap: var(--fluent-space-xxs); + pointer-events: none; + height: 100%; + min-height: unset; + flex-wrap: nowrap; + flex: 1; + margin: 0; + padding-block: 0; + padding-inline: 3.5px 0; + list-style: none; + display: flex; + overflow: hidden; + -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + text-overflow: ellipsis; + white-space: nowrap; + align-items: center; + gap: var(--fluent-space-xxs); + pointer-events: none; + height: 100%; + min-height: unset; + flex-wrap: nowrap; + flex: 1; + margin: 0; + padding-block: 0; + padding-inline: 3.5px 0; + list-style: none; + display: flex; + overflow: hidden; + -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to right, #000 calc(100% - 24px), #0000 100%); +} + +html[dir="rtl"] cbi-dropdown > ul.preview, body[data-fluent-dir="rtl"] cbi-dropdown > ul.preview, html[dir="rtl"] cbi-dropdown > ul:not(.dropdown), body[data-fluent-dir="rtl"] cbi-dropdown > ul:not(.dropdown), html[dir="rtl"] .cbi-dropdown > ul.preview, body[data-fluent-dir="rtl"] .cbi-dropdown > ul.preview, html[dir="rtl"] .cbi-dropdown > ul:not(.dropdown), body[data-fluent-dir="rtl"] .cbi-dropdown > ul:not(.dropdown), html[dir="rtl"] .fluent-custom-select > ul.preview, body[data-fluent-dir="rtl"] .fluent-custom-select > ul.preview, html[dir="rtl"] .fluent-custom-select > ul:not(.dropdown), body[data-fluent-dir="rtl"] .fluent-custom-select > ul:not(.dropdown) { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +html[dir="rtl"] cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +body[data-fluent-dir="rtl"] cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +html[dir="rtl"] .cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +body[data-fluent-dir="rtl"] .cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +html[dir="rtl"] .fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +body[data-fluent-dir="rtl"] .fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + -webkit-mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); + mask-image: linear-gradient(to left, #000 calc(100% - 24px), #0000 100%); +} + +cbi-dropdown > ul.preview > li, cbi-dropdown > ul:not(.dropdown) > li, .cbi-dropdown > ul.preview > li, .cbi-dropdown > ul:not(.dropdown) > li, .fluent-custom-select > ul.preview > li, .fluent-custom-select > ul:not(.dropdown) > li { + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + flex-shrink: 0; + margin: 0; + line-height: 20px; + display: none; + overflow: hidden; + padding: 0 !important; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + flex-shrink: 0; + margin: 0; + line-height: 20px; + display: none; + overflow: hidden; + padding: 0 !important; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + flex-shrink: 0; + margin: 0; + line-height: 20px; + display: none; + overflow: hidden; + padding: 0 !important; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; + flex-shrink: 0; + margin: 0; + line-height: 20px; + display: none; + overflow: hidden; + padding: 0 !important; +} + +cbi-dropdown > ul.preview > li form, cbi-dropdown > ul.preview > li input[type="checkbox"], cbi-dropdown > ul:not(.dropdown) > li form, cbi-dropdown > ul:not(.dropdown) > li input[type="checkbox"], .cbi-dropdown > ul.preview > li form, .cbi-dropdown > ul.preview > li input[type="checkbox"], .cbi-dropdown > ul:not(.dropdown) > li form, .cbi-dropdown > ul:not(.dropdown) > li input[type="checkbox"], .fluent-custom-select > ul.preview > li form, .fluent-custom-select > ul.preview > li input[type="checkbox"], .fluent-custom-select > ul:not(.dropdown) > li form, .fluent-custom-select > ul:not(.dropdown) > li input[type="checkbox"] { + display: none !important; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li form { + display: none !important; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li input[type="checkbox"] { + display: none !important; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li form { + display: none !important; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li input[type="checkbox"] { + display: none !important; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li form { + display: none !important; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li input[type="checkbox"] { + display: none !important; +} + +cbi-dropdown > ul.preview > li, cbi-dropdown > ul:not(.dropdown) > li, .cbi-dropdown > ul.preview > li, .cbi-dropdown > ul:not(.dropdown) > li, .fluent-custom-select > ul.preview > li, .fluent-custom-select > ul:not(.dropdown) > li { + border-inline-start: none !important; + padding-inline-start: 0 !important; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + border-inline-start: none !important; + padding-inline-start: 0 !important; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + border-inline-start: none !important; + padding-inline-start: 0 !important; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + border-inline-start: none !important; + padding-inline-start: 0 !important; +} + +cbi-dropdown > ul.preview > li:before, cbi-dropdown > ul:not(.dropdown) > li:before, .cbi-dropdown > ul.preview > li:before, .cbi-dropdown > ul:not(.dropdown) > li:before, .fluent-custom-select > ul.preview > li:before, .fluent-custom-select > ul:not(.dropdown) > li:before { + background-color: #0000 !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li:before { + background-color: #0000 !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li:before { + background-color: #0000 !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li:before { + background-color: #0000 !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +cbi-dropdown > ul.preview > li[display], cbi-dropdown > ul.preview > li[selected], cbi-dropdown > ul:not(.dropdown) > li[display], cbi-dropdown > ul:not(.dropdown) > li[selected], .cbi-dropdown > ul.preview > li[display], .cbi-dropdown > ul.preview > li[selected], .cbi-dropdown > ul:not(.dropdown) > li[display], .cbi-dropdown > ul:not(.dropdown) > li[selected], .fluent-custom-select > ul.preview > li[display], .fluent-custom-select > ul.preview > li[selected], .fluent-custom-select > ul:not(.dropdown) > li[display], .fluent-custom-select > ul:not(.dropdown) > li[selected] { + align-items: center; + display: inline-flex; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] { + align-items: center; + display: inline-flex; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[selected] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[selected] { + align-items: center; + display: inline-flex; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] { + align-items: center; + display: inline-flex; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[selected] { + align-items: center; + display: inline-flex; +} + +cbi-dropdown > ul.preview > li[display] ~ li[selected]:before, cbi-dropdown > ul:not(.dropdown) > li[display] ~ li[selected]:before, .cbi-dropdown > ul.preview > li[display] ~ li[selected]:before, .cbi-dropdown > ul:not(.dropdown) > li[display] ~ li[selected]:before, .fluent-custom-select > ul.preview > li[display] ~ li[selected]:before, .fluent-custom-select > ul:not(.dropdown) > li[display] ~ li[selected]:before { + content: ", "; + color: var(--fluent-text-secondary); + margin-inline-end: 2px; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] ~ li[selected]:before { + content: ", "; + color: var(--fluent-text-secondary); + margin-inline-end: 2px; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] ~ li[selected]:before { + content: ", "; + color: var(--fluent-text-secondary); + margin-inline-end: 2px; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] ~ li[selected]:before { + content: ", "; + color: var(--fluent-text-secondary); + margin-inline-end: 2px; +} + +cbi-dropdown > ul.preview > li[placeholder], cbi-dropdown > ul:not(.dropdown) > li[placeholder], .cbi-dropdown > ul.preview > li[placeholder], .cbi-dropdown > ul:not(.dropdown) > li[placeholder], .fluent-custom-select > ul.preview > li[placeholder], .fluent-custom-select > ul:not(.dropdown) > li[placeholder] { + color: var(--fluent-text-disabled); +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[placeholder] { + color: var(--fluent-text-disabled); +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[placeholder] { + color: var(--fluent-text-disabled); +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[placeholder] { + color: var(--fluent-text-disabled); +} + +cbi-dropdown > ul.preview:not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +cbi-dropdown > ul:not(.dropdown):not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +.cbi-dropdown > ul.preview:not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +.cbi-dropdown > ul:not(.dropdown):not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +.fluent-custom-select > ul.preview:not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +.fluent-custom-select > ul:not(.dropdown):not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:not(:has(li[selected])):not(:has(li[display])) > li[placeholder] { + color: var(--fluent-text-disabled); + display: inline; +} + +cbi-dropdown > ul.preview > li[placeholder]:only-of-type, cbi-dropdown > ul:not(.dropdown) > li[placeholder]:only-of-type, .cbi-dropdown > ul.preview > li[placeholder]:only-of-type, .cbi-dropdown > ul:not(.dropdown) > li[placeholder]:only-of-type, .fluent-custom-select > ul.preview > li[placeholder]:only-of-type, .fluent-custom-select > ul:not(.dropdown) > li[placeholder]:only-of-type { + color: var(--fluent-text-disabled); + align-items: center; + display: inline-flex; +} + +cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[placeholder]:only-of-type { + color: var(--fluent-text-disabled); + align-items: center; + display: inline-flex; +} + +.cbi-dropdown:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[placeholder]:only-of-type { + color: var(--fluent-text-disabled); + align-items: center; + display: inline-flex; +} + +.fluent-custom-select:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[placeholder]:only-of-type { + color: var(--fluent-text-disabled); + align-items: center; + display: inline-flex; +} + +cbi-dropdown:has(ul.preview > li[selected]:not([placeholder])) > ul.preview > li[placeholder] { + display: none; +} + +cbi-dropdown:has(ul.preview > li[display]:not([placeholder])) > ul.preview > li[placeholder] { + display: none; +} + +cbi-dropdown:has(ul:not(.dropdown) > li[selected]:not([placeholder])) > ul:not(.dropdown) > li[placeholder] { + display: none; +} + +cbi-dropdown:has(ul:not(.dropdown) > li[display]:not([placeholder])) > ul:not(.dropdown) > li[placeholder] { + display: none; +} + +.cbi-dropdown:has(ul.preview > li[selected]:not([placeholder])) > ul.preview > li[placeholder] { + display: none; +} + +.cbi-dropdown:has(ul.preview > li[display]:not([placeholder])) > ul.preview > li[placeholder] { + display: none; +} + +.cbi-dropdown:has(ul:not(.dropdown) > li[selected]:not([placeholder])) > ul:not(.dropdown) > li[placeholder] { + display: none; +} + +.cbi-dropdown:has(ul:not(.dropdown) > li[display]:not([placeholder])) > ul:not(.dropdown) > li[placeholder] { + display: none; +} + +.fluent-custom-select:has(ul.preview > li[selected]:not([placeholder])) > ul.preview > li[placeholder] { + display: none; +} + +.fluent-custom-select:has(ul.preview > li[display]:not([placeholder])) > ul.preview > li[placeholder] { + display: none; +} + +.fluent-custom-select:has(ul:not(.dropdown) > li[selected]:not([placeholder])) > ul:not(.dropdown) > li[placeholder] { + display: none; +} + +.fluent-custom-select:has(ul:not(.dropdown) > li[display]:not([placeholder])) > ul:not(.dropdown) > li[placeholder] { + display: none; +} + +cbi-dropdown > span.open, .cbi-dropdown > span.open, .fluent-custom-select > span.open { + cursor: pointer; + width: 24px; + min-width: 24px; + height: 24px; + color: var(--fluent-text-secondary); + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard); + z-index: 1; + flex-shrink: 0; + justify-content: center; + align-items: center; + margin-inline-end: 0; + display: inline-flex; + position: relative; + margin-inline-start: auto !important; + font-size: 0 !important; +} + +cbi-dropdown > span.open:before, .cbi-dropdown > span.open:before, .fluent-custom-select > span.open:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-chevron-down-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-chevron-down-20-regular); + mask-image: var(--fluent-mask-chevron-down-20-regular); + -webkit-user-select: none; + user-select: none; + content: ""; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard),transform .2s cubic-bezier(.4, 0, .23, 1); + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +cbi-dropdown > span.open svg, .cbi-dropdown > span.open svg, .fluent-custom-select > span.open svg { + fill: currentColor; + width: 16px; + height: 16px; + color: inherit; + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard); +} + +cbi-dropdown > span.open svg path, .cbi-dropdown > span.open svg path, .fluent-custom-select > span.open svg path { + fill: currentColor; +} + +cbi-dropdown > span.open:has(svg):before { + display: none !important; +} + +.cbi-dropdown > span.open:has(svg):before { + display: none !important; +} + +.fluent-custom-select > span.open:has(svg):before { + display: none !important; +} + +cbi-dropdown:hover > span.open, .cbi-dropdown:hover > span.open, .fluent-custom-select:hover > span.open { + color: var(--fluent-text); +} + +cbi-dropdown[disabled] > span.open, .cbi-dropdown[disabled] > span.open, .fluent-custom-select[disabled] > span.open { + color: var(--fluent-border-hover); +} + +cbi-dropdown[open] > span.open:before, .cbi-dropdown[open] > span.open:before, .fluent-custom-select[open] > span.open:before { + transform: rotate(180deg); +} + +cbi-dropdown > span.more, .cbi-dropdown > span.more, .fluent-custom-select > span.more { + text-overflow: ellipsis; + white-space: nowrap; + color: var(--fluent-text-disabled); + pointer-events: none; + font-size: var(--fluent-font-size-base); + flex: 1; + align-items: center; + padding-block: 0; + padding-inline: 3.5px 0; + line-height: 20px; + display: inline-flex; + overflow: hidden; +} + +cbi-dropdown.btn, cbi-dropdown.cbi-button, .cbi-dropdown.btn, .cbi-dropdown.cbi-button, .fluent-custom-select.btn, .fluent-custom-select.cbi-button { + gap: 0; + padding: 0; + overflow: visible; +} + +cbi-dropdown.btn > ul.preview, cbi-dropdown.btn > ul:not(.dropdown), cbi-dropdown.btn > span.more, cbi-dropdown.cbi-button > ul.preview, cbi-dropdown.cbi-button > ul:not(.dropdown), cbi-dropdown.cbi-button > span.more, .cbi-dropdown.btn > ul.preview, .cbi-dropdown.btn > ul:not(.dropdown), .cbi-dropdown.btn > span.more, .cbi-dropdown.cbi-button > ul.preview, .cbi-dropdown.cbi-button > ul:not(.dropdown), .cbi-dropdown.cbi-button > span.more, .fluent-custom-select.btn > ul.preview, .fluent-custom-select.btn > ul:not(.dropdown), .fluent-custom-select.btn > span.more, .fluent-custom-select.cbi-button > ul.preview, .fluent-custom-select.cbi-button > ul:not(.dropdown), .fluent-custom-select.cbi-button > span.more { + min-width: 0; + padding: 0 var(--fluent-space-md); + align-self: stretch; +} + +cbi-dropdown.btn > span.open, cbi-dropdown.cbi-button > span.open, .cbi-dropdown.btn > span.open, .cbi-dropdown.cbi-button > span.open, .fluent-custom-select.btn > span.open, .fluent-custom-select.cbi-button > span.open { + width: calc(var(--fluent-btn-height) - 2px); + min-width: calc(var(--fluent-btn-height) - 2px); + border-inline-start: 1px solid color-mix(in srgb, currentColor 30%, transparent); + background: none; + align-self: stretch; + height: auto; + margin-inline-start: 0; +} + +cbi-dropdown.btn:hover > span.open, cbi-dropdown.cbi-button:hover > span.open, .cbi-dropdown.btn:hover > span.open, .cbi-dropdown.cbi-button:hover > span.open, .fluent-custom-select.btn:hover > span.open, .fluent-custom-select.cbi-button:hover > span.open, cbi-dropdown.btn:active > span.open, cbi-dropdown.btn.focus > span.open, cbi-dropdown.btn:focus-within > span.open, cbi-dropdown.btn[open] > span.open, cbi-dropdown.cbi-button:active > span.open, cbi-dropdown.cbi-button.focus > span.open, cbi-dropdown.cbi-button:focus-within > span.open, cbi-dropdown.cbi-button[open] > span.open, .cbi-dropdown.btn:active > span.open, .cbi-dropdown.btn.focus > span.open, .cbi-dropdown.btn:focus-within > span.open, .cbi-dropdown.btn[open] > span.open, .cbi-dropdown.cbi-button:active > span.open, .cbi-dropdown.cbi-button.focus > span.open, .cbi-dropdown.cbi-button:focus-within > span.open, .cbi-dropdown.cbi-button[open] > span.open, .fluent-custom-select.btn:active > span.open, .fluent-custom-select.btn.focus > span.open, .fluent-custom-select.btn:focus-within > span.open, .fluent-custom-select.btn[open] > span.open, .fluent-custom-select.cbi-button:active > span.open, .fluent-custom-select.cbi-button.focus > span.open, .fluent-custom-select.cbi-button:focus-within > span.open, .fluent-custom-select.cbi-button[open] > span.open { + background: none; +} + +cbi-dropdown:not([empty]) > span.more, .cbi-dropdown:not([empty]) > span.more, .fluent-custom-select:not([empty]) > span.more { + display: none; +} + +cbi-dropdown[empty] > ul.preview, cbi-dropdown[empty] > ul:not(.dropdown), cbi-dropdown[empty]:not([open]) > ul.dropdown, .cbi-dropdown[empty] > ul.preview, .cbi-dropdown[empty] > ul:not(.dropdown), .cbi-dropdown[empty]:not([open]) > ul.dropdown, .fluent-custom-select[empty] > ul.preview, .fluent-custom-select[empty] > ul:not(.dropdown), .fluent-custom-select[empty]:not([open]) > ul.dropdown { + display: none !important; +} + +cbi-dropdown:not([open]):has( > ul:not(.dropdown)) > ul.dropdown { + display: none !important; +} + +cbi-dropdown:not([open]):has( > ul.preview) > ul.dropdown:not(.preview) { + display: none !important; +} + +.cbi-dropdown:not([open]):has( > ul:not(.dropdown)) > ul.dropdown { + display: none !important; +} + +.cbi-dropdown:not([open]):has( > ul.preview) > ul.dropdown:not(.preview) { + display: none !important; +} + +.fluent-custom-select:not([open]):has( > ul:not(.dropdown)) > ul.dropdown { + display: none !important; +} + +.fluent-custom-select:not([open]):has( > ul.preview) > ul.dropdown:not(.preview) { + display: none !important; +} + +cbi-dropdown > ul.dropdown:not(.preview), .cbi-dropdown > ul.dropdown:not(.preview), .fluent-custom-select > ul.dropdown:not(.preview) { + margin: 0; + padding: 0; + list-style: none; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview), .cbi-dropdown[open] > ul.dropdown:not(.preview), .fluent-custom-select[open] > ul.dropdown:not(.preview) { + box-sizing: border-box; + background-color: var(--fluent-dropdown-bg); + border: 1px solid var(--fluent-dropdown-border); + border-radius: var(--fluent-radius-md); + box-shadow: var(--fluent-dropdown-shadow); + min-width: 100%; + z-index: var(--fluent-zindex-dropdown); + padding: var(--fluent-space-xxs); + margin: 0; + margin-top: var(--fluent-space-xxs); + transform-origin: top; + outline: 1px solid #0000; + flex-direction: column; + list-style: none; + animation: .2s cubic-bezier(.4, 0, .23, 1) forwards fluent-dropdown-slide-in; + display: flex; + position: absolute; + overflow-y: auto; + max-width: 80vw !important; + max-height: min(var(--fluent-dropdown-max-height-limit),var(--fluent-dropdown-max-height),45dvh) !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar { + width: 8px; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar { + width: 8px; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview)::-webkit-scrollbar { + width: 8px; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-track { + background: none; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-track { + background: none; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-track { + background: none; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-thumb { + background-color: var(--fluent-scrollbar-thumb-bg); + border-radius: var(--fluent-radius-md); + background-clip: padding-box; + border: 2px solid #0000; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-thumb { + background-color: var(--fluent-scrollbar-thumb-bg); + border-radius: var(--fluent-radius-md); + background-clip: padding-box; + border: 2px solid #0000; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-thumb { + background-color: var(--fluent-scrollbar-thumb-bg); + border-radius: var(--fluent-radius-md); + background-clip: padding-box; + border: 2px solid #0000; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-thumb:hover { + background-color: var(--fluent-scrollbar-thumb-bg-hover); +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-thumb:hover { + background-color: var(--fluent-scrollbar-thumb-bg-hover); +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview)::-webkit-scrollbar-thumb:hover { + background-color: var(--fluent-scrollbar-thumb-bg-hover); +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li { + box-sizing: border-box; + width: 100%; + min-height: var(--fluent-input-height); + padding: 5px var(--fluent-space-xs); + border-radius: var(--fluent-radius-md); + color: var(--fluent-text); + cursor: pointer; + -webkit-user-select: none; + user-select: none; + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + text-overflow: ellipsis; + white-space: nowrap; + flex-shrink: 0; + align-items: center; + line-height: 20px; + display: flex; + position: relative; + overflow: hidden; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(.hide-close) { + white-space: normal !important; + text-overflow: clip !important; + align-items: flex-start !important; + height: auto !important; + overflow: visible !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(div) { + white-space: normal !important; + text-overflow: clip !important; + align-items: flex-start !important; + height: auto !important; + overflow: visible !important; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(.hide-close) { + white-space: normal !important; + text-overflow: clip !important; + align-items: flex-start !important; + height: auto !important; + overflow: visible !important; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(div) { + white-space: normal !important; + text-overflow: clip !important; + align-items: flex-start !important; + height: auto !important; + overflow: visible !important; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview) > li:has(.hide-close) { + white-space: normal !important; + text-overflow: clip !important; + align-items: flex-start !important; + height: auto !important; + overflow: visible !important; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview) > li:has(div) { + white-space: normal !important; + text-overflow: clip !important; + align-items: flex-start !important; + height: auto !important; + overflow: visible !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(.hide-close) label { + gap: var(--fluent-space-xs); + white-space: normal !important; + align-items: center !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(div) label { + gap: var(--fluent-space-xs); + white-space: normal !important; + align-items: center !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(.hide-close) label { + gap: var(--fluent-space-xs); + white-space: normal !important; + align-items: center !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(div) label { + gap: var(--fluent-space-xs); + white-space: normal !important; + align-items: center !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview) > li:has(.hide-close) label { + gap: var(--fluent-space-xs); + white-space: normal !important; + align-items: center !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview) > li:has(div) label { + gap: var(--fluent-space-xs); + white-space: normal !important; + align-items: center !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(.hide-close) .hide-close { + word-break: break-word; + min-width: unset !important; + white-space: normal !important; + width: 100% !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(div) .hide-close { + word-break: break-word; + min-width: unset !important; + white-space: normal !important; + width: 100% !important; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(.hide-close) .hide-close { + word-break: break-word; + min-width: unset !important; + white-space: normal !important; + width: 100% !important; +} + +.cbi-dropdown[open] > ul.dropdown:not(.preview) > li:has(div) .hide-close { + word-break: break-word; + min-width: unset !important; + white-space: normal !important; + width: 100% !important; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview) > li:has(.hide-close) .hide-close { + word-break: break-word; + min-width: unset !important; + white-space: normal !important; + width: 100% !important; +} + +.fluent-custom-select[open] > ul.dropdown:not(.preview) > li:has(div) .hide-close { + word-break: break-word; + min-width: unset !important; + white-space: normal !important; + width: 100% !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li { + transition: background-color 50ms var(--fluent-easing-standard),color 50ms var(--fluent-easing-standard); +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li > span, cbi-dropdown[open] > ul.dropdown:not(.preview) > li > .ifacebadge, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li > span, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li > .ifacebadge, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li > span, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li > .ifacebadge { + align-items: center; + gap: var(--fluent-space-xxs); + width: 100%; + display: flex; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li:hover, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li:hover, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li:hover { + background-color: var(--fluent-dropdown-option-hover); + color: var(--fluent-text); +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[selected], .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[selected], .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[selected] { + background-color: var(--fluent-dropdown-option-selected); + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-semibold); +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[selected]:hover, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[selected]:hover, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[selected]:hover { + background-color: var(--fluent-primary-lighter); +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li.focus, cbi-dropdown[open] > ul.dropdown:not(.preview) > li:active, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li.focus, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li:active, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li.focus, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li:active { + background-color: var(--fluent-bg-active); + color: var(--fluent-text); +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[disabled], cbi-dropdown[open] > ul.dropdown:not(.preview) > li[unselectable], .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[disabled], .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[unselectable], .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[disabled], .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[unselectable] { + color: var(--fluent-text-disabled); + cursor: not-allowed; + pointer-events: none; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"], .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"], .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[data-value="-"] { + cursor: default; + pointer-events: all; + padding: var(--fluent-space-xxs) !important; + background-color: #0000 !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"] form, cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"] input[type="checkbox"], .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"] form, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"] input[type="checkbox"], .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[data-value="-"] form, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[data-value="-"] input[type="checkbox"] { + display: none !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"], .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"], .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[data-value="-"] { + padding-inline-start: var(--fluent-space-xxs) !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"]:before, cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"]:after, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"]:before, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li[data-value="-"]:after, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[data-value="-"]:before, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li[data-value="-"]:after { + content: none !important; + display: none !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li .create-item-input, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li .create-item-input, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li .create-item-input { + width: 100%; + min-height: 24px; + padding: 2px var(--fluent-space-xs); + box-sizing: border-box; + background-color: var(--fluent-bg); + border: 1px solid var(--fluent-input-border); + border-radius: var(--fluent-radius-sm); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + color: var(--fluent-text); + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + outline: none; +} + +cbi-dropdown > select, .cbi-dropdown > select, .fluent-custom-select > select { + box-sizing: border-box; + width: 100%; + cursor: pointer; + padding-inline-end: var(--fluent-space-xl); +} + +cbi-dropdown > .cbi-input-select, .cbi-dropdown > .cbi-input-select, .fluent-custom-select > .cbi-input-select { + background-image: linear-gradient(var(--fluent-input-border-bottom-focus), var(--fluent-input-border-bottom-focus)),url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMCIgaGVpZ2h0PSI2IiB2aWV3Qm94PSIwIDAgMTAgNiIgZmlsbD0ibm9uZSI+CiAgPHBhdGggZD0iTTEgMWw0IDQgNC00IiBzdHJva2U9IiM2MTYxNjEiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==); + background-position: bottom, right 10px center; + background-repeat: no-repeat; + background-size: 0 2px, 10px 6px; + background-origin: border-box; +} + +html[dir="rtl"] cbi-dropdown > .cbi-input-select, body[data-fluent-dir="rtl"] cbi-dropdown > .cbi-input-select, html[dir="rtl"] .cbi-dropdown > .cbi-input-select, body[data-fluent-dir="rtl"] .cbi-dropdown > .cbi-input-select, html[dir="rtl"] .fluent-custom-select > .cbi-input-select, body[data-fluent-dir="rtl"] .fluent-custom-select > .cbi-input-select { + background-position: bottom, 10px; +} + +cbi-dropdown > .cbi-input-select, .cbi-dropdown > .cbi-input-select, .fluent-custom-select > .cbi-input-select { + transition: background-size var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + background-repeat: no-repeat; + background-origin: border-box; +} + +cbi-dropdown > .cbi-input-select:hover:not([disabled]):not(:focus), .cbi-dropdown > .cbi-input-select:hover:not([disabled]):not(:focus), .fluent-custom-select > .cbi-input-select:hover:not([disabled]):not(:focus) { + border-color: var(--fluent-input-border-hover); + border-bottom-color: var(--fluent-input-border-bottom-hover); +} + +cbi-dropdown > .cbi-input-select:focus, .cbi-dropdown > .cbi-input-select:focus, .fluent-custom-select > .cbi-input-select:focus { + border-bottom-color: var(--fluent-input-border-bottom-focus); + background-size: 100% 2px, 10px 6px; + outline: none; +} + +cbi-dropdown > .cbi-input-select, .cbi-dropdown > .cbi-input-select, .fluent-custom-select > .cbi-input-select { + box-sizing: border-box; + width: 100%; + cursor: pointer; + padding-inline-end: var(--fluent-space-xl); +} + +cbi-dropdown .ifacebadge, .cbi-dropdown .ifacebadge, .fluent-custom-select .ifacebadge { + box-shadow: none !important; + color: inherit !important; + min-height: unset !important; + background: none !important; + border: none !important; + padding: 0 !important; +} + +cbi-dropdown li .hide-close, .cbi-dropdown li .hide-close, .fluent-custom-select li .hide-close { + display: none !important; +} + +cbi-dropdown li .hide-open, .cbi-dropdown li .hide-open, .fluent-custom-select li .hide-open { + display: inline-block; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li .hide-open, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li .hide-open, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li .hide-open { + display: none !important; +} + +cbi-dropdown[open] > ul.dropdown:not(.preview) > li .hide-close, .cbi-dropdown[open] > ul.dropdown:not(.preview) > li .hide-close, .fluent-custom-select[open] > ul.dropdown:not(.preview) > li .hide-close { + display: inline-block !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li form, .cbi-dropdown ul.dropdown:not(.preview) > li form, .fluent-custom-select ul.dropdown:not(.preview) > li form { + flex-shrink: 0; + align-items: center !important; + margin: 0 !important; + padding: 0 !important; + display: inline-flex !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li label, .cbi-dropdown ul.dropdown:not(.preview) > li label, .fluent-custom-select ul.dropdown:not(.preview) > li label { + font-weight: inherit; + color: inherit; + white-space: nowrap; + text-overflow: ellipsis; + gap: var(--fluent-space-xs); + flex: 1; + overflow: hidden; + cursor: pointer !important; + align-items: center !important; + margin: 0 !important; + padding: 0 !important; + display: inline-flex !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"], .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"], .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"] { + appearance: none !important; + border: 1px solid var(--fluent-checkbox-border) !important; + background-color: var(--fluent-checkbox-bg) !important; + width: 16px !important; + min-width: 16px !important; + height: 16px !important; + min-height: 16px !important; + cursor: pointer !important; + vertical-align: middle !important; + box-sizing: border-box !important; + background-image: none !important; + border-radius: 4px !important; + justify-content: center !important; + align-items: center !important; + margin: 0 !important; + margin-inline-end: var(--fluent-space-sm) !important; + transition: background-color .1s, border-color .1s !important; + display: inline-flex !important; + position: relative !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:before, .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:before, .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:before { + content: none !important; + display: none !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:hover:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:hover:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:hover:not(:disabled) { + border-color: var(--fluent-checkbox-hover-border) !important; + background-color: var(--fluent-bg-hover) !important; + background-image: none !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:disabled, .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:disabled, .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:disabled { + background-color: var(--fluent-input-bg-disabled) !important; + border-color: var(--fluent-border-hover) !important; + cursor: not-allowed !important; + opacity: 1 !important; + background-image: none !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"], .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"], .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"] { + pointer-events: none; + border-radius: var(--fluent-radius-sm) !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"], cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked, cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:hover, cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:hover, cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:focus, cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:focus, cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:active, cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:active, cbi-dropdown ul.dropdown:not(.preview) > li:hover input[type="checkbox"]:checked, cbi-dropdown ul.dropdown:not(.preview) > li[selected]:hover input[type="checkbox"], cbi-dropdown ul.dropdown:not(.preview) > li.focus input[type="checkbox"]:checked, cbi-dropdown ul.dropdown:not(.preview) > li[selected].focus input[type="checkbox"], .cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"], .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked, .cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:hover, .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:hover, .cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:focus, .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:focus, .cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:active, .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:active, .cbi-dropdown ul.dropdown:not(.preview) > li:hover input[type="checkbox"]:checked, .cbi-dropdown ul.dropdown:not(.preview) > li[selected]:hover input[type="checkbox"], .cbi-dropdown ul.dropdown:not(.preview) > li.focus input[type="checkbox"]:checked, .cbi-dropdown ul.dropdown:not(.preview) > li[selected].focus input[type="checkbox"], .fluent-custom-select ul.dropdown:not(.preview) > li[selected] input[type="checkbox"], .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:checked, .fluent-custom-select ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:hover, .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:hover, .fluent-custom-select ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:focus, .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:focus, .fluent-custom-select ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:active, .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:active, .fluent-custom-select ul.dropdown:not(.preview) > li:hover input[type="checkbox"]:checked, .fluent-custom-select ul.dropdown:not(.preview) > li[selected]:hover input[type="checkbox"], .fluent-custom-select ul.dropdown:not(.preview) > li.focus input[type="checkbox"]:checked, .fluent-custom-select ul.dropdown:not(.preview) > li[selected].focus input[type="checkbox"] { + border: 1px solid var(--fluent-primary) !important; + background-color: var(--fluent-primary) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:hover:not(:disabled), cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:hover:not(:disabled), cbi-dropdown ul.dropdown:not(.preview) > li[selected]:hover input[type="checkbox"]:not(:disabled), cbi-dropdown ul.dropdown:not(.preview) > li:hover input[type="checkbox"]:checked:not(:disabled), cbi-dropdown ul.dropdown:not(.preview) > li[selected].focus input[type="checkbox"]:not(:disabled), cbi-dropdown ul.dropdown:not(.preview) > li.focus input[type="checkbox"]:checked:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:hover:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:hover:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li[selected]:hover input[type="checkbox"]:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li:hover input[type="checkbox"]:checked:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li[selected].focus input[type="checkbox"]:not(:disabled), .cbi-dropdown ul.dropdown:not(.preview) > li.focus input[type="checkbox"]:checked:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:hover:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:hover:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li[selected]:hover input[type="checkbox"]:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li:hover input[type="checkbox"]:checked:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li[selected].focus input[type="checkbox"]:not(:disabled), .fluent-custom-select ul.dropdown:not(.preview) > li.focus input[type="checkbox"]:checked:not(:disabled) { + background-color: var(--fluent-primary-hover) !important; + border-color: var(--fluent-primary-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; +} + +cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:disabled, cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:disabled, .cbi-dropdown ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:disabled, .cbi-dropdown ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:disabled, .fluent-custom-select ul.dropdown:not(.preview) > li[selected] input[type="checkbox"]:disabled, .fluent-custom-select ul.dropdown:not(.preview) > li input[type="checkbox"]:checked:disabled { + background-color: var(--fluent-border-hover) !important; + border-color: var(--fluent-border-hover) !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZmlsbD0id2hpdGUiIGQ9Ik0xMy44NTQgMy42NDZhLjUuNSAwIDAgMSAwIC43MDhsLTcgN2EuNS41IDAgMCAxLS43MDggMGwtMy41LTMuNWEuNS41IDAgMSAxIC43MDgtLjcwOEw2LjUgMTAuMjkzbDYuNjQ2LTYuNjQ3YS41LjUgMCAwIDEgLjcwOCAweiIvPgo8L3N2Zz4K) !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: 12px 12px !important; +} + +cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li { + padding-inline-start: 32px; + position: relative !important; +} + +cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li:before { + --icon-check: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+CiAgPHBhdGggZD0iTTEzLjg1NCAzLjY0NmEuNS41IDAgMCAxIDAgLjcwOGwtNyA3YS41LjUgMCAwIDEtLjcwOCAwbC0zLjUtMy41YS41LjUgMCAxIDEgLjcwOC0uNzA4TDYuNSAxMC4yOTNsNi42NDYtNi42NDdhLjUuNSAwIDAgMSAuNzA4IDB6Ii8+Cjwvc3ZnPgo=); + content: "" !important; + inset-inline-start: 8px !important; + z-index: 2 !important; + width: 16px !important; + height: 16px !important; + -webkit-mask-image: var(--icon-check) !important; + mask-image: var(--icon-check) !important; + background-color: #0000 !important; + transition: background-color .1s !important; + position: absolute !important; + top: 50% !important; + transform: translateY(-50%) !important; + -webkit-mask-position: center !important; + mask-position: center !important; + -webkit-mask-size: contain !important; + mask-size: contain !important; + -webkit-mask-repeat: no-repeat !important; + mask-repeat: no-repeat !important; +} + +cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[selected]:before { + background-color: var(--fluent-text) !important; +} + +cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, .cbi-dropdown:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[disabled][selected]:before, .fluent-custom-select:not([multiple]) > ul.dropdown:not(.preview) > li[unselectable][selected]:before { + background-color: var(--fluent-text-disabled) !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) { + background: none !important; + padding: 0 !important; + padding-inline-start: 0 !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) { + background: none !important; + padding: 0 !important; + padding-inline-start: 0 !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) { + background: none !important; + padding: 0 !important; + padding-inline-start: 0 !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge):before { + content: none !important; + display: none !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge):before { + content: none !important; + display: none !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge):before { + content: none !important; + display: none !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) > form { + margin-inline-start: var(--fluent-space-xs) !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) > form { + margin-inline-start: var(--fluent-space-xs) !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) > form { + margin-inline-start: var(--fluent-space-xs) !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge { + justify-content: flex-start !important; + align-items: center !important; + gap: 4px var(--fluent-space-xs) !important; + width: 100% !important; + height: 100% !important; + min-height: var(--fluent-input-height) !important; + box-sizing: border-box !important; + border-radius: inherit !important; + box-shadow: none !important; + padding: 5px var(--fluent-space-xs) !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + flex-wrap: wrap !important; + margin: 0 !important; + display: flex !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge { + justify-content: flex-start !important; + align-items: center !important; + gap: 4px var(--fluent-space-xs) !important; + width: 100% !important; + height: 100% !important; + min-height: var(--fluent-input-height) !important; + box-sizing: border-box !important; + border-radius: inherit !important; + box-shadow: none !important; + padding: 5px var(--fluent-space-xs) !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + flex-wrap: wrap !important; + margin: 0 !important; + display: flex !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge { + justify-content: flex-start !important; + align-items: center !important; + gap: 4px var(--fluent-space-xs) !important; + width: 100% !important; + height: 100% !important; + min-height: var(--fluent-input-height) !important; + box-sizing: border-box !important; + border-radius: inherit !important; + box-shadow: none !important; + padding: 5px var(--fluent-space-xs) !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + flex-wrap: wrap !important; + margin: 0 !important; + display: flex !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + align-items: center !important; + line-height: 1.2 !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + align-items: center !important; + line-height: 1.2 !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + align-items: center !important; + line-height: 1.2 !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + align-items: center !important; + line-height: 1.2 !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + align-items: center !important; + line-height: 1.2 !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + align-items: center !important; + line-height: 1.2 !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge { + color: var(--fluent-zonebadge-text) !important; + min-height: unset !important; + border-radius: var(--fluent-radius-sm) !important; + height: auto !important; + line-height: 1.2 !important; + font-size: var(--fluent-font-size-xs) !important; + background: #fff9 !important; + border-color: #0000001a !important; + justify-content: center !important; + align-items: center !important; + gap: 4px !important; + padding: 1px 5px !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge { + color: var(--fluent-zonebadge-text) !important; + min-height: unset !important; + border-radius: var(--fluent-radius-sm) !important; + height: auto !important; + line-height: 1.2 !important; + font-size: var(--fluent-font-size-xs) !important; + background: #fff9 !important; + border-color: #0000001a !important; + justify-content: center !important; + align-items: center !important; + gap: 4px !important; + padding: 1px 5px !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge { + color: var(--fluent-zonebadge-text) !important; + min-height: unset !important; + border-radius: var(--fluent-radius-sm) !important; + height: auto !important; + line-height: 1.2 !important; + font-size: var(--fluent-font-size-xs) !important; + background: #fff9 !important; + border-color: #0000001a !important; + justify-content: center !important; + align-items: center !important; + gap: 4px !important; + padding: 1px 5px !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge em { + color: inherit !important; + line-height: inherit !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge strong { + color: inherit !important; + line-height: inherit !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge span { + color: inherit !important; + line-height: inherit !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge em { + color: inherit !important; + line-height: inherit !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge strong { + color: inherit !important; + line-height: inherit !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge span { + color: inherit !important; + line-height: inherit !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge em { + color: inherit !important; + line-height: inherit !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge strong { + color: inherit !important; + line-height: inherit !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge span { + color: inherit !important; + line-height: inherit !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge img { + vertical-align: middle !important; + background: currentColor !important; + width: 14px !important; + height: 14px !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge img { + vertical-align: middle !important; + background: currentColor !important; + width: 14px !important; + height: 14px !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge) .zonebadge .ifacebadge img { + vertical-align: middle !important; + background: currentColor !important; + width: 14px !important; + height: 14px !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge):hover .zonebadge { + background-image: linear-gradient(#0000000d, #0000000d) !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge):hover .zonebadge { + background-image: linear-gradient(#0000000d, #0000000d) !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge):hover .zonebadge { + background-image: linear-gradient(#0000000d, #0000000d) !important; +} + +cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge).focus .zonebadge { + background-image: linear-gradient(#00000014, #00000014) !important; +} + +.cbi-dropdown[open] > ul:not(.preview) > li:has(.zonebadge).focus .zonebadge { + background-image: linear-gradient(#00000014, #00000014) !important; +} + +.fluent-custom-select[open] > ul:not(.preview) > li:has(.zonebadge).focus .zonebadge { + background-image: linear-gradient(#00000014, #00000014) !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge) { + display: none; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge) { + display: none; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge) { + display: none; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge) { + display: none; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge) { + display: none; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge) { + display: none; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge)[selected] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge)[display] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge)[selected] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge)[display] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge)[selected] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge)[display] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge)[selected] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge)[display] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge)[selected] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge)[display] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge)[selected] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge)[display] { + border-radius: var(--fluent-radius-sm) !important; + background: none !important; + border: none !important; + align-items: center !important; + margin: 2px !important; + padding: 0 !important; + display: inline-flex !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge { + padding: 2px var(--fluent-space-sm) !important; + border-radius: var(--fluent-radius-sm) !important; + height: 20px !important; + min-height: 20px !important; + font-size: var(--fluent-font-size-sm) !important; + box-shadow: none !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + align-items: center !important; + padding-inline-start: var(--fluent-space-sm) !important; + display: inline-flex !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge { + padding: 2px var(--fluent-space-sm) !important; + border-radius: var(--fluent-radius-sm) !important; + height: 20px !important; + min-height: 20px !important; + font-size: var(--fluent-font-size-sm) !important; + box-shadow: none !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + align-items: center !important; + padding-inline-start: var(--fluent-space-sm) !important; + display: inline-flex !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge { + padding: 2px var(--fluent-space-sm) !important; + border-radius: var(--fluent-radius-sm) !important; + height: 20px !important; + min-height: 20px !important; + font-size: var(--fluent-font-size-sm) !important; + box-shadow: none !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + align-items: center !important; + padding-inline-start: var(--fluent-space-sm) !important; + display: inline-flex !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge { + padding: 2px var(--fluent-space-sm) !important; + border-radius: var(--fluent-radius-sm) !important; + height: 20px !important; + min-height: 20px !important; + font-size: var(--fluent-font-size-sm) !important; + box-shadow: none !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + align-items: center !important; + padding-inline-start: var(--fluent-space-sm) !important; + display: inline-flex !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge) .zonebadge { + padding: 2px var(--fluent-space-sm) !important; + border-radius: var(--fluent-radius-sm) !important; + height: 20px !important; + min-height: 20px !important; + font-size: var(--fluent-font-size-sm) !important; + box-shadow: none !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + align-items: center !important; + padding-inline-start: var(--fluent-space-sm) !important; + display: inline-flex !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge) .zonebadge { + padding: 2px var(--fluent-space-sm) !important; + border-radius: var(--fluent-radius-sm) !important; + height: 20px !important; + min-height: 20px !important; + font-size: var(--fluent-font-size-sm) !important; + box-shadow: none !important; + color: var(--fluent-zonebadge-text) !important; + border: none !important; + align-items: center !important; + padding-inline-start: var(--fluent-space-sm) !important; + display: inline-flex !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + font-size: inherit !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + font-size: inherit !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge span { + color: inherit !important; + font-size: inherit !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + font-size: inherit !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + font-size: inherit !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge span { + color: inherit !important; + font-size: inherit !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + font-size: inherit !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + font-size: inherit !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge span { + color: inherit !important; + font-size: inherit !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + font-size: inherit !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + font-size: inherit !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge span { + color: inherit !important; + font-size: inherit !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + font-size: inherit !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + font-size: inherit !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge) .zonebadge span { + color: inherit !important; + font-size: inherit !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge) .zonebadge strong { + color: inherit !important; + font-size: inherit !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge) .zonebadge em { + color: inherit !important; + font-size: inherit !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge) .zonebadge span { + color: inherit !important; + font-size: inherit !important; +} + +cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge .ifacebadge { + display: none !important; +} + +cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge .ifacebadge { + display: none !important; +} + +.cbi-dropdown:not([open]) > ul > li:has(.zonebadge) .zonebadge .ifacebadge { + display: none !important; +} + +.cbi-dropdown[open] > ul.preview > li:has(.zonebadge) .zonebadge .ifacebadge { + display: none !important; +} + +.fluent-custom-select:not([open]) > ul > li:has(.zonebadge) .zonebadge .ifacebadge { + display: none !important; +} + +.fluent-custom-select[open] > ul.preview > li:has(.zonebadge) .zonebadge .ifacebadge { + display: none !important; +} + +cbi-dropdown.fluent-custom-select[data-open-direction="up"][open] > ul.dropdown:not(.preview), .cbi-dropdown.fluent-custom-select[data-open-direction="up"][open] > ul.dropdown:not(.preview), .fluent-custom-select.fluent-custom-select[data-open-direction="up"][open] > ul.dropdown:not(.preview) { + width: max-content; + min-width: 100%; + bottom: 100%; + margin-top: 0 !important; + margin-bottom: var(--fluent-space-xxs) !important; + transform-origin: bottom !important; + top: auto !important; + animation: .2s cubic-bezier(.4, 0, .23, 1) forwards fluent-dropdown-slide-in-up !important; + inset-inline-end: auto !important; +} + +cbi-dropdown.fluent-custom-select[data-open-direction="down"][open] > ul.dropdown:not(.preview), .cbi-dropdown.fluent-custom-select[data-open-direction="down"][open] > ul.dropdown:not(.preview), .fluent-custom-select.fluent-custom-select[data-open-direction="down"][open] > ul.dropdown:not(.preview) { + width: max-content; + min-width: 100%; + top: 100%; + margin-bottom: 0 !important; + margin-top: var(--fluent-space-xxs) !important; + transform-origin: top !important; + bottom: auto !important; + animation: .2s cubic-bezier(.4, 0, .23, 1) forwards fluent-dropdown-slide-in !important; + inset-inline-end: auto !important; +} + +cbi-dropdown.fluent-custom-select[data-fluent-floating="modal"][open] > ul.dropdown:not(.preview), .cbi-dropdown.fluent-custom-select[data-fluent-floating="modal"][open] > ul.dropdown:not(.preview), .fluent-custom-select.fluent-custom-select[data-fluent-floating="modal"][open] > ul.dropdown:not(.preview) { + min-width: var(--fluent-dropdown-width); + max-height: var(--fluent-dropdown-max-height); + z-index: calc(var(--fluent-zindex-dropdown) + 1); + position: fixed; + top: var(--fluent-dropdown-top) !important; + bottom: auto !important; + margin: 0 !important; + inset-inline-start: var(--fluent-dropdown-left) !important; + inset-inline-end: auto !important; +} + +@supports (anchor-name: --a) { + cbi-dropdown[open] > ul.dropdown:not(.preview), .cbi-dropdown[open] > ul.dropdown:not(.preview), .fluent-custom-select[open] > ul.dropdown:not(.preview) { + position-anchor: --fluent-cbi-dropdown-active !important; + position-try-fallbacks: flip-block !important; + min-width: anchor-size(width) !important; + max-height: min(var(--fluent-dropdown-max-height-limit),var(--fluent-dropdown-max-height),45dvh) !important; + margin: 0 !important; + margin-top: var(--fluent-space-xxs) !important; + left: anchor(left) !important; + position: fixed !important; + right: auto !important; + } + + html[dir="rtl"] cbi-dropdown[open] > ul.dropdown:not(.preview), body[data-fluent-dir="rtl"] cbi-dropdown[open] > ul.dropdown:not(.preview), html[dir="rtl"] .cbi-dropdown[open] > ul.dropdown:not(.preview), body[data-fluent-dir="rtl"] .cbi-dropdown[open] > ul.dropdown:not(.preview), html[dir="rtl"] .fluent-custom-select[open] > ul.dropdown:not(.preview), body[data-fluent-dir="rtl"] .fluent-custom-select[open] > ul.dropdown:not(.preview) { + right: anchor(right) !important; + left: auto !important; + } + + cbi-dropdown[open] > ul.dropdown:not(.preview), .cbi-dropdown[open] > ul.dropdown:not(.preview), .fluent-custom-select[open] > ul.dropdown:not(.preview) { + top: anchor(bottom) !important; + bottom: auto !important; + } + + cbi-dropdown[open] > ul.dropdown:not(.preview)[style*="bottom"], .cbi-dropdown[open] > ul.dropdown:not(.preview)[style*="bottom"], .fluent-custom-select[open] > ul.dropdown:not(.preview)[style*="bottom"] { + top: auto !important; + bottom: anchor(top) !important; + margin-top: 0 !important; + margin-bottom: var(--fluent-space-xxs) !important; + max-height: min(var(--fluent-dropdown-max-height-limit),var(--fluent-dropdown-max-height),45dvh) !important; + transform-origin: bottom !important; + animation: .2s cubic-bezier(.4, 0, .23, 1) forwards fluent-dropdown-slide-in-up !important; + } +} + +cbi-dropdown[data-open-direction="up"][open] > ul.dropdown:not(.preview), .cbi-dropdown[data-open-direction="up"][open] > ul.dropdown:not(.preview), .fluent-custom-select[data-open-direction="up"][open] > ul.dropdown:not(.preview) { + margin-top: 0 !important; + margin-bottom: var(--fluent-space-xxs) !important; + transform-origin: bottom !important; + animation: .2s cubic-bezier(.4, 0, .23, 1) forwards fluent-dropdown-slide-in-up !important; + top: auto !important; + bottom: 100% !important; +} + +@supports (anchor-name: --a) { + cbi-dropdown[data-open-direction="up"][open] > ul.dropdown:not(.preview), .cbi-dropdown[data-open-direction="up"][open] > ul.dropdown:not(.preview), .fluent-custom-select[data-open-direction="up"][open] > ul.dropdown:not(.preview) { + top: auto !important; + bottom: anchor(top) !important; + margin-top: 0 !important; + margin-bottom: var(--fluent-space-xxs) !important; + max-height: min(var(--fluent-dropdown-max-height-limit),var(--fluent-dropdown-max-height),45dvh) !important; + } +} + +.cbi-value-field.cbi-dropdown-open { + position: relative; + z-index: 2000 !important; +} + +.cbi-value:has(.cbi-dropdown-open) { + position: relative; + z-index: 2000 !important; +} + +.cbi-section:has(.cbi-dropdown-open) { + position: relative; + z-index: 2000 !important; +} + +.cbi-map:has(.cbi-dropdown-open) { + position: relative; + z-index: 2000 !important; +} + +cbi-dropdown:not([optional]) > ul.dropdown > li[placeholder]:not(:only-child), .cbi-dropdown:not([optional]) > ul.dropdown > li[placeholder]:not(:only-child), .fluent-custom-select:not([optional]) > ul.dropdown > li[placeholder]:not(:only-child) { + display: none; +} + +.cbi-dropdown.cbi-button, .cbi-dropdown.btn { + border-bottom-color: var(--fluent-btn-secondary-border); + --menu-btn-bg-default: var(--fluent-btn-secondary-bg); + --menu-btn-bg-hover: var(--fluent-btn-secondary-bg-hover); + --menu-btn-bg-active: var(--fluent-btn-secondary-bg-active); + background: var(--menu-btn-bg-default); + box-sizing: border-box; + vertical-align: middle; + cursor: pointer; + justify-content: center; + align-items: center; + display: inline-flex; + position: relative; +} + +html[dir="rtl"] .cbi-dropdown.cbi-button, body[data-fluent-dir="rtl"] .cbi-dropdown.cbi-button, html[dir="rtl"] .cbi-dropdown.btn, body[data-fluent-dir="rtl"] .cbi-dropdown.btn { + direction: rtl; +} + +.cbi-dropdown.cbi-button, .cbi-dropdown.btn { + min-width: unset; + width: auto; + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + gap: 0 !important; + padding: 0 !important; +} + +.cbi-dropdown.cbi-button:after, .cbi-dropdown.btn:after { + content: none !important; + display: none !important; +} + +.cbi-dropdown.cbi-button:hover:not([disabled]), .cbi-dropdown.btn:hover:not([disabled]) { + background: var(--menu-btn-bg-default); + border-color: var(--fluent-btn-secondary-border-hover) !important; + border-bottom-color: var(--fluent-btn-secondary-border-hover) !important; +} + +.cbi-dropdown.cbi-button:active, .cbi-dropdown.cbi-button:active:not([disabled]), .cbi-dropdown.btn:active, .cbi-dropdown.btn:active:not([disabled]) { + background: var(--menu-btn-bg-default); + border-color: var(--fluent-btn-secondary-border-active) !important; + border-bottom-color: var(--fluent-btn-secondary-border-active) !important; +} + +.cbi-dropdown.cbi-button:focus, .cbi-dropdown.cbi-button.focus, .cbi-dropdown.cbi-button[open], .cbi-dropdown.btn:focus, .cbi-dropdown.btn.focus, .cbi-dropdown.btn[open] { + outline: none; + border-color: var(--fluent-btn-secondary-border-active) !important; + border-bottom-color: var(--fluent-btn-secondary-border-active) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative), .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative) { + --menu-btn-bg-default: var(--fluent-btn-primary-bg); + --menu-btn-bg-hover: var(--fluent-btn-primary-bg-hover); + --menu-btn-bg-active: var(--fluent-btn-primary-bg-active); + background: var(--menu-btn-bg-default); + border: 0 !important; +} + +.cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative):hover:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]), .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative):active, .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative):active:not([disabled]) { + background: var(--menu-btn-bg-default); + border: 0 !important; +} + +.cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative)[open], .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative):focus, .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative).focus, .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative)[open] { + border: 0 !important; +} + +.cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li, .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative) > ul.preview > li, .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative) > ul:not(.dropdown) > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative):not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.cbi-button.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.cbi-button.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.cbi-button.important:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.btn.cbi-button-apply:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.btn.cbi-button-save:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.btn.cbi-button-positive:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before, .cbi-dropdown.btn.important:not(.cbi-button-remove):not(.cbi-button-negative) > span.open:before { + border-color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-remove, .cbi-dropdown.cbi-button.cbi-button-negative, .cbi-dropdown.btn.cbi-button-remove, .cbi-dropdown.btn.cbi-button-negative { + --menu-btn-bg-default: var(--fluent-error); + --menu-btn-bg-hover: var(--fluent-error-hover); + --menu-btn-bg-active: var(--fluent-error-active); + background: var(--menu-btn-bg-default); + color: var(--fluent-text-on-primary); + border: 0 !important; +} + +.cbi-dropdown.cbi-button.cbi-button-remove:hover:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-negative:hover:not([disabled]), .cbi-dropdown.btn.cbi-button-remove:hover:not([disabled]), .cbi-dropdown.btn.cbi-button-negative:hover:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-remove:active, .cbi-dropdown.cbi-button.cbi-button-remove:active:not([disabled]), .cbi-dropdown.cbi-button.cbi-button-negative:active, .cbi-dropdown.cbi-button.cbi-button-negative:active:not([disabled]), .cbi-dropdown.btn.cbi-button-remove:active, .cbi-dropdown.btn.cbi-button-remove:active:not([disabled]), .cbi-dropdown.btn.cbi-button-negative:active, .cbi-dropdown.btn.cbi-button-negative:active:not([disabled]) { + background: var(--menu-btn-bg-default); + border: 0 !important; +} + +.cbi-dropdown.cbi-button.cbi-button-remove:focus, .cbi-dropdown.cbi-button.cbi-button-remove.focus, .cbi-dropdown.cbi-button.cbi-button-remove[open], .cbi-dropdown.cbi-button.cbi-button-negative:focus, .cbi-dropdown.cbi-button.cbi-button-negative.focus, .cbi-dropdown.cbi-button.cbi-button-negative[open], .cbi-dropdown.btn.cbi-button-remove:focus, .cbi-dropdown.btn.cbi-button-remove.focus, .cbi-dropdown.btn.cbi-button-remove[open], .cbi-dropdown.btn.cbi-button-negative:focus, .cbi-dropdown.btn.cbi-button-negative.focus, .cbi-dropdown.btn.cbi-button-negative[open] { + border: 0 !important; +} + +.cbi-dropdown.cbi-button.cbi-button-remove > ul.preview > li, .cbi-dropdown.cbi-button.cbi-button-remove > ul:not(.dropdown) > li, .cbi-dropdown.cbi-button.cbi-button-negative > ul.preview > li, .cbi-dropdown.cbi-button.cbi-button-negative > ul:not(.dropdown) > li, .cbi-dropdown.btn.cbi-button-remove > ul.preview > li, .cbi-dropdown.btn.cbi-button-remove > ul:not(.dropdown) > li, .cbi-dropdown.btn.cbi-button-negative > ul.preview > li, .cbi-dropdown.btn.cbi-button-negative > ul:not(.dropdown) > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-remove:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-negative:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.btn.cbi-button-remove:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.btn.cbi-button-negative:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button.cbi-button-remove > span.open:before, .cbi-dropdown.cbi-button.cbi-button-negative > span.open:before, .cbi-dropdown.btn.cbi-button-remove > span.open:before, .cbi-dropdown.btn.cbi-button-negative > span.open:before { + border-color: var(--fluent-text-on-primary) !important; +} + +.cbi-dropdown.cbi-button > ul.preview, .cbi-dropdown.cbi-button > ul:not(.dropdown), .cbi-dropdown.cbi-button > span.more, .cbi-dropdown.btn > ul.preview, .cbi-dropdown.btn > ul:not(.dropdown), .cbi-dropdown.btn > span.more { + border-start-start-radius: calc(var(--fluent-radius-md) - 1px); + border-end-start-radius: calc(var(--fluent-radius-md) - 1px); + align-self: stretch; + pointer-events: auto !important; + flex: 1 !important; + padding-inline-start: var(--fluent-space-md) !important; + padding-inline-end: var(--fluent-space-sm) !important; + transition: none !important; +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + border-start-start-radius: calc(var(--fluent-radius-md) - 1px); + border-end-start-radius: calc(var(--fluent-radius-md) - 1px); + align-self: stretch; + pointer-events: auto !important; + flex: 1 !important; + padding-inline-start: var(--fluent-space-md) !important; + padding-inline-end: var(--fluent-space-sm) !important; + transition: none !important; +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + border-start-start-radius: calc(var(--fluent-radius-md) - 1px); + border-end-start-radius: calc(var(--fluent-radius-md) - 1px); + align-self: stretch; + pointer-events: auto !important; + flex: 1 !important; + padding-inline-start: var(--fluent-space-md) !important; + padding-inline-end: var(--fluent-space-sm) !important; + transition: none !important; +} + +.cbi-dropdown.cbi-button > ul.preview:hover:not([disabled]), .cbi-dropdown.cbi-button > ul:not(.dropdown):hover:not([disabled]), .cbi-dropdown.cbi-button > span.more:hover:not([disabled]), .cbi-dropdown.btn > ul.preview:hover:not([disabled]), .cbi-dropdown.btn > ul:not(.dropdown):hover:not([disabled]), .cbi-dropdown.btn > span.more:hover:not([disabled]) { + background: var(--menu-btn-bg-hover); +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:hover:not([disabled]) { + background: var(--menu-btn-bg-hover); +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:hover:not([disabled]) { + background: var(--menu-btn-bg-hover); +} + +.cbi-dropdown.cbi-button > ul.preview:active:not([disabled]), .cbi-dropdown.cbi-button > ul:not(.dropdown):active:not([disabled]), .cbi-dropdown.cbi-button > span.more:active:not([disabled]), .cbi-dropdown.btn > ul.preview:active:not([disabled]), .cbi-dropdown.btn > ul:not(.dropdown):active:not([disabled]), .cbi-dropdown.btn > span.more:active:not([disabled]) { + background: var(--menu-btn-bg-active); +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:active:not([disabled]) { + background: var(--menu-btn-bg-active); +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown:active:not([disabled]) { + background: var(--menu-btn-bg-active); +} + +.cbi-dropdown.cbi-button > ul.preview, .cbi-dropdown.cbi-button > ul:not(.dropdown), .cbi-dropdown.btn > ul.preview, .cbi-dropdown.btn > ul:not(.dropdown) { + height: 100%; + min-height: unset; + align-items: center; + margin: 0; + padding-block: 0; + display: flex; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + height: 100%; + min-height: unset; + align-items: center; + margin: 0; + padding-block: 0; + display: flex; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + height: 100%; + min-height: unset; + align-items: center; + margin: 0; + padding-block: 0; + display: flex; + -webkit-mask-image: none !important; + mask-image: none !important; +} + +.cbi-dropdown.cbi-button > ul.preview > li, .cbi-dropdown.cbi-button > ul:not(.dropdown) > li, .cbi-dropdown.btn > ul.preview > li, .cbi-dropdown.btn > ul:not(.dropdown) > li { + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + color: inherit; +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + color: inherit; +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + color: inherit; +} + +.cbi-dropdown.cbi-button > ul.preview > li[selected], .cbi-dropdown.cbi-button > ul.preview > li[display], .cbi-dropdown.cbi-button > ul:not(.dropdown) > li[selected], .cbi-dropdown.cbi-button > ul:not(.dropdown) > li[display], .cbi-dropdown.btn > ul.preview > li[selected], .cbi-dropdown.btn > ul.preview > li[display], .cbi-dropdown.btn > ul:not(.dropdown) > li[selected], .cbi-dropdown.btn > ul:not(.dropdown) > li[display] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[selected] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown.cbi-button:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[selected] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown.btn:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li[display] { + align-items: center; + display: inline-flex; +} + +.cbi-dropdown.cbi-button > span.open, .cbi-dropdown.btn > span.open { + width: 30px; + height: 100%; + color: inherit; + border-start-end-radius: calc(var(--fluent-radius-md) - 1px); + border-end-end-radius: calc(var(--fluent-radius-md) - 1px); + justify-content: center; + align-items: center; + margin-inline-end: 0; + font-size: 0; + display: inline-flex; + pointer-events: auto !important; + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + margin-inline-start: 0 !important; +} + +.cbi-dropdown.cbi-button > span.open:before, .cbi-dropdown.btn > span.open:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-chevron-down-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-chevron-down-20-regular); + mask-image: var(--fluent-mask-chevron-down-20-regular); + -webkit-user-select: none; + user-select: none; + content: ""; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; + transition: transform .2s cubic-bezier(.4, 0, .23, 1) !important; +} + +.cbi-dropdown.cbi-button > span.open svg, .cbi-dropdown.btn > span.open svg { + width: 16px; + height: 16px; + color: inherit !important; +} + +.cbi-dropdown.cbi-button > span.open:has(svg):before { + display: none !important; +} + +.cbi-dropdown.btn > span.open:has(svg):before { + display: none !important; +} + +.cbi-dropdown.cbi-button > span.open:hover:not([disabled]), .cbi-dropdown.btn > span.open:hover:not([disabled]) { + background: var(--menu-btn-bg-hover); +} + +.cbi-dropdown.cbi-button > span.open:active:not([disabled]), .cbi-dropdown.btn > span.open:active:not([disabled]), .cbi-dropdown.cbi-button[open] > span.open, .cbi-dropdown.btn[open] > span.open { + background: var(--menu-btn-bg-active); +} + +.cbi-dropdown.cbi-button[open] > span.open:before, .cbi-dropdown.btn[open] > span.open:before { + transform: rotate(180deg); +} + +.cbi-dropdown.cbi-button:focus-visible, .cbi-dropdown.btn:focus-visible { + outline: 2px solid var(--fluent-focus-color); + outline-offset: 2px; +} + +.cbi-dropdown.cbi-button[disabled], .cbi-dropdown.btn[disabled] { + opacity: 1; + cursor: not-allowed; + background: var(--fluent-bg-active) !important; + border-color: var(--fluent-bg-active) !important; + border-bottom-color: var(--fluent-bg-active) !important; + color: var(--fluent-text-disabled) !important; + pointer-events: none !important; +} + +.cbi-dropdown.cbi-button[disabled] *, .cbi-dropdown.btn[disabled] * { + pointer-events: none !important; +} + +.cbi-dropdown.cbi-button[disabled] > ul.preview > li, .cbi-dropdown.cbi-button[disabled] > ul:not(.dropdown) > li, .cbi-dropdown.btn[disabled] > ul.preview > li, .cbi-dropdown.btn[disabled] > ul:not(.dropdown) > li { + color: var(--fluent-text-disabled) !important; +} + +.cbi-dropdown.cbi-button[disabled]:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-disabled) !important; +} + +.cbi-dropdown.btn[disabled]:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown > li { + color: var(--fluent-text-disabled) !important; +} + +.cbi-dropdown.cbi-button[disabled] > span.open:before, .cbi-dropdown.btn[disabled] > span.open:before { + border-color: var(--fluent-text-disabled) !important; +} + +.cbi-dropdown.cbi-button.spinning, .cbi-dropdown.btn.spinning { + pointer-events: none !important; + padding-inline-start: 0 !important; +} + +.cbi-dropdown.cbi-button.spinning *, .cbi-dropdown.btn.spinning * { + pointer-events: none !important; +} + +.cbi-dropdown.cbi-button.spinning:before, .cbi-dropdown.btn.spinning:before { + content: ""; + inset-inline-start: 12px; + --size: 16px; + --indicator-size: 2px; + height: var(--size); + width: var(--size); + box-sizing: border-box; + border: var(--indicator-size) solid color-mix(in srgb, currentColor 20%, transparent); + z-index: 2; + background: none; + border-top-color: currentColor; + border-right-color: currentColor; + border-radius: 50%; + animation: 1s infinite fluent-spin-swing; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.cbi-dropdown.cbi-button.spinning > ul.preview, .cbi-dropdown.cbi-button.spinning > ul:not(.dropdown), .cbi-dropdown.cbi-button.spinning > span.more, .cbi-dropdown.btn.spinning > ul.preview, .cbi-dropdown.btn.spinning > ul:not(.dropdown), .cbi-dropdown.btn.spinning > span.more { + padding-inline-start: 36px !important; +} + +.cbi-dropdown.cbi-button.spinning:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + padding-inline-start: 36px !important; +} + +.cbi-dropdown.btn.spinning:not([open]):not(:has( > ul:not(.dropdown))):not(:has( > ul.preview)) > ul.dropdown { + padding-inline-start: 36px !important; +} + +.cbi-dynlist { + align-items: stretch; + gap: var(--fluent-space-sm, 8px); + cursor: text; + flex-direction: column; + width: 100%; + max-width: 400px; + min-height: 30px; + line-height: 1.3; + display: flex; + position: relative; +} + +.cbi-dynlist[name="sshkeys"], .cbi-dynlist[name="sshkeys"] > .item { + max-width: none; +} + +.cbi-dynlist > .item { + pointer-events: auto; + width: 100%; + max-width: 100%; + color: var(--fluent-text); + cursor: move; + -webkit-user-select: text; + user-select: text; + outline: 0; + display: block; + position: relative; +} + +.cbi-dynlist > .item.drag-over { + border-top: 1px solid var(--fluent-primary); +} + +.cbi-dynlist > .item.dragging { + opacity: .5; +} + +.cbi-dynlist > .item > span { + pointer-events: none; + white-space: normal; + word-break: break-all; + height: var(--fluent-input-height); + padding-block: 0; + padding-inline: var(--fluent-space-sm) 42px; + color: var(--fluent-text); + background: var(--fluent-bg-hover); + border: 1px solid var(--fluent-border); + border-bottom-width: 2px; + border-bottom-color: var(--fluent-border); + border-radius: var(--fluent-radius-md); + box-sizing: border-box; + outline: 0; + align-items: center; + display: flex; +} + +.cbi-dynlist > .item:hover > span { + background: var(--fluent-bg-active); + border-color: var(--fluent-border-hover); + border-bottom-color: var(--fluent-border-hover); +} + +.cbi-dynlist > .item:after { + content: ""; + inset-inline-end: 2px; + pointer-events: auto; + border-radius: var(--fluent-radius-round, 50%); + cursor: pointer; + z-index: 2; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard); + background-color: #0000; + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; + border: none; + outline: 0; + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 28px !important; + height: 28px !important; +} + +.cbi-dynlist > .item:after:hover { + background-color: var(--fluent-error-light); + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +.cbi-dynlist > .item:after:active { + background-color: var(--fluent-error-hover); + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +[data-theme="dark"] .cbi-dynlist > .item:after { + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +[data-theme="dark"] .cbi-dynlist > .item:after:hover { + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +[data-theme="dark"] .cbi-dynlist > .item:after:active { + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 16px 16px; +} + +.cbi-dynlist > .add-item { + box-sizing: border-box; + align-items: center; + gap: 0; + width: 100%; + min-width: 0; + max-width: 100%; + display: flex; +} + +.cbi-dynlist > .add-item > input, .cbi-dynlist > .add-item > .cbi-dropdown { + box-sizing: border-box; + flex: auto; + width: 100%; + min-width: 0; + max-width: 100%; +} + +.cbi-dynlist > .add-item > input { + border-inline-end: none; + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.cbi-dynlist > .add-item > input:focus { + border-inline-end: none; +} + +.cbi-dynlist > .add-item > input.drag-over { + border-top: 1px solid var(--fluent-primary); +} + +.cbi-dynlist > .add-item > .cbi-dropdown { + height: var(--fluent-input-height); + min-height: var(--fluent-input-height); +} + +.cbi-dynlist > .add-item > .cbi-button-add { + flex: 0 0 var(--fluent-input-height); + max-width: var(--fluent-input-height); + height: var(--fluent-input-height); + border-bottom-width: 2px; + border-bottom-color: var(--fluent-btn-primary-bg); + cursor: pointer; + box-sizing: border-box; + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 14px 14px; + border-start-start-radius: 0; + border-start-end-radius: var(--fluent-radius-md); + border-end-end-radius: var(--fluent-radius-md); + border-end-start-radius: 0; + justify-content: center; + align-items: center; + display: flex; + width: var(--fluent-input-height) !important; + min-width: var(--fluent-input-height) !important; + color: #0000 !important; + padding: 0 !important; + font-size: 0 !important; +} + +.cbi-dynlist > .add-item > .cbi-button-add:hover { + background-color: var(--fluent-btn-primary-bg-hover); + border-color: var(--fluent-btn-primary-bg-hover); + border-bottom-color: var(--fluent-btn-primary-bg-hover); + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 14px 14px; +} + +.cbi-dynlist > .add-item > .cbi-button-add:active { + background-color: var(--fluent-btn-primary-bg-active); + border-color: var(--fluent-btn-primary-bg-active); + border-bottom-color: var(--fluent-btn-primary-bg-active); + background-image: url("data:image/svg+xml,"); + background-position: center; + background-repeat: no-repeat; + background-size: 14px 14px; +} + +.cbi-dynlist > .add-item > .cbi-button-add.drag-over { + border-top: 1px solid var(--fluent-primary); +} + +.cbi-dynlist > .add-item:has( > .cbi-button-add) > input { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.cbi-dynlist > .add-item:has( > .cbi-button-add) > .cbi-dropdown { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.cbi-dynlist > .add-item:not(:has( > .cbi-button-add)) > input { + border-radius: var(--fluent-radius-md); +} + +.cbi-dynlist > .add-item:not(:has( > .cbi-button-add)) > .cbi-dropdown { + border-radius: var(--fluent-radius-md); +} + +.cbi-dynlist > .cbi-dynlist > .add-item[ondrop] > input { + min-width: 13rem; +} + +.cbi-dynlist cbi-dropdown > ul.dropdown > li[placeholder], .cbi-dynlist .cbi-dropdown > ul.dropdown > li[placeholder] { + display: none !important; +} + +.cbi-dynlist cbi-dropdown > ul.dropdown > li, .cbi-dynlist .cbi-dropdown > ul.dropdown > li { + padding-inline-start: var(--fluent-space-xs) !important; +} + +.cbi-dynlist cbi-dropdown > ul.dropdown > li:before, .cbi-dynlist cbi-dropdown > ul.dropdown > li:after, .cbi-dynlist .cbi-dropdown > ul.dropdown > li:before, .cbi-dynlist .cbi-dropdown > ul.dropdown > li:after { + content: none !important; + display: none !important; +} + +.control-group:has( > input.cbi-input-password) > input.cbi-input-password + .cbi-button { + justify-content: center; + align-items: center; + display: inline-flex; + width: var(--fluent-input-height) !important; + padding: 0 !important; + font-size: 0 !important; +} + +.control-group:has( > input.cbi-input-password[type="password"]) > input.cbi-input-password + .cbi-button:after { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-eye-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-eye-20-regular); + mask-image: var(--fluent-mask-eye-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +.control-group:has( > input.cbi-input-password[type="password"]) > input.cbi-input-password + .cbi-button:hover:after { + background: var(--fluent-text) !important; +} + +.control-group:has( > input.cbi-input-password[type="text"]) > input.cbi-input-password + .cbi-button:after { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-eye-off-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-eye-off-20-regular); + mask-image: var(--fluent-mask-eye-off-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +.control-group:has( > input.cbi-input-password[type="text"]) > input.cbi-input-password + .cbi-button:hover:after { + background: var(--fluent-text) !important; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) { + position: relative; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)):hover:not([disabled]):not(:focus):not(:focus-within):not(.focus):not([open]) { + border-color: var(--fluent-input-border-hover); + border-bottom-color: var(--fluent-input-border-bottom-hover); +} + +:is(.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)):focus, .control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)):focus-within, .control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)).focus, .control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button))[open]) { + border-bottom-color: var(--fluent-input-border-bottom-focus); + outline: none; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)):focus:after { + transform: scaleX(1); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)):focus-within:after { + transform: scaleX(1); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)).focus:after { + transform: scaleX(1); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button))[open]:after { + transform: scaleX(1); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)):after { + box-sizing: border-box; + content: ""; + inset-inline-start: 1px; + bottom: -1px; + background: var(--fluent-input-border-bottom-focus); + height: 2px; + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard); + pointer-events: none; + border-end-end-radius: var(--fluent-radius-sm); + border-end-start-radius: var(--fluent-radius-sm); + position: absolute; + inset-inline-end: 1px; + transform: scaleX(0); +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) { + align-items: stretch; + gap: 0; + display: flex; + padding: 0 !important; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) > input.cbi-input-password { + min-width: 0; + color: var(--fluent-text); + flex: 1; + box-shadow: none !important; + height: 100% !important; + padding: 0 var(--fluent-space-sm) !important; + background: none !important; + border: none !important; + outline: none !important; + margin: 0 !important; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) > input.cbi-input-password + .cbi-button { + border-start-start-radius: 0; + border-start-end-radius: var(--fluent-radius-md); + border-end-end-radius: var(--fluent-radius-md); + border-end-start-radius: 0; + flex-shrink: 0; + box-shadow: none !important; + background: none !important; + border: none !important; + height: 100% !important; + margin: 0 !important; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) > input.cbi-input-password + .cbi-button:hover { + background: var(--fluent-bg-hover) !important; +} + +.control-group:has( > input.cbi-input-password):not(:has( > .cbi-button ~ .cbi-button)) > input.cbi-input-password + .cbi-button:active { + background: var(--fluent-bg-active) !important; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button) { + align-items: center; + gap: 0; + display: flex; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button) > input.cbi-input-password { + flex: 1; + min-width: 0; + border-inline-end: none !important; + border-start-end-radius: 0 !important; + border-end-end-radius: 0 !important; + margin: 0 !important; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button) > input.cbi-input-password + .cbi-button { + flex-shrink: 0; + background: var(--fluent-input-bg) !important; + border-inline-start: none !important; + border-color: var(--fluent-input-border) !important; + border-bottom-color: var(--fluent-input-border-bottom) !important; + border-start-start-radius: 0 !important; + border-end-start-radius: 0 !important; + margin: 0 !important; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button) > input.cbi-input-password + .cbi-button:hover { + background: var(--fluent-bg-hover) !important; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button) > .cbi-button ~ .cbi-button { + margin-inline-start: var(--fluent-space-sm) !important; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button):has( > input.cbi-input-password:hover) > input.cbi-input-password + .cbi-button { + border-color: var(--fluent-input-border-hover) !important; + border-bottom-color: var(--fluent-input-border-bottom-hover) !important; +} + +.control-group:has( > input.cbi-input-password):has( > .cbi-button ~ .cbi-button):has( > input.cbi-input-password:focus) > input.cbi-input-password + .cbi-button { + border-bottom-color: var(--fluent-input-border-bottom-focus) !important; +} + +.password-input { + align-items: stretch; + display: flex; + position: relative; +} + +.password-input input { + box-sizing: border-box; + width: 100%; + flex: 1; + padding-inline-end: var(--fluent-space-xl); +} + +.password-input .toggle-password { + inset-inline-end: 1px; + height: calc(var(--fluent-input-height) - 2px); + width: calc(var(--fluent-input-height) - 2px); + color: var(--fluent-text-secondary); + cursor: pointer; + background: none; + border: none; + border-start-start-radius: 0; + border-start-end-radius: var(--fluent-radius-md); + border-end-end-radius: var(--fluent-radius-md); + border-end-start-radius: 0; + padding: 0; + position: absolute; + top: 1px; +} + +.password-input .toggle-password:hover { + color: var(--fluent-text); + background: var(--fluent-bg-hover); +} + +.password-input .toggle-password svg { + width: 16px; + height: 16px; +} + +.password-strength { + gap: var(--fluent-space-xxs); + margin-top: var(--fluent-space-xxs); + display: flex; +} + +.password-strength .strength-bar { + background: var(--fluent-border); + height: 3px; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); + border-radius: 2px; + flex: 1; +} + +.password-strength.strength-weak .strength-bar:nth-child(-n+1) { + background: var(--fluent-error); +} + +.password-strength.strength-medium .strength-bar:nth-child(-n+2) { + background: var(--fluent-warning); +} + +.password-strength.strength-strong .strength-bar { + background: var(--fluent-success); +} + +input[type="text"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="text"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="text"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +input[type="password"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="password"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="password"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +input[type="email"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="email"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="email"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +input[type="number"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="number"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="number"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +input[type="search"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="search"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="search"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +input[type="tel"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="tel"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="tel"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +input[type="url"].cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +input[type="url"].cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +input[type="url"].cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +select.cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +select.cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +select.cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +.cbi-dropdown.cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +.cbi-dropdown.cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +.cbi-dropdown.cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +.fluent-custom-select.cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +.fluent-custom-select.cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +.fluent-custom-select.cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +.cbi-input-text.cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +.cbi-input-text.cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +.cbi-input-text.cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +.cbi-input-password.cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +.cbi-input-password.cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +.cbi-input-password.cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +.password-input.cbi-input-invalid { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + background-image: linear-gradient(0deg, var(--fluent-error) 0%, var(--fluent-error) 100%) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; +} + +.password-input.cbi-input-invalid:hover:not(:focus) { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +.password-input.cbi-input-invalid:focus { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; + box-shadow: 0 0 0 1px var(--fluent-error) inset !important; + background-size: 100% 2px !important; +} + +.fluent-error-message { + color: var(--fluent-error); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-regular); + animation: fluent-error-fade-in .15s var(--fluent-easing-standard) forwards; + align-items: center; + gap: 6px; + margin-top: 4px; + line-height: 16px; + display: flex; +} + +.fluent-error-message svg { + color: var(--fluent-error); + flex-shrink: 0; +} + +@keyframes fluent-error-fade-in { + from { + opacity: 0; + transform: translateY(-4px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +input[data-tooltip-style="error"], .cbi-input-invalid[data-tooltip-style="error"] { + border-color: var(--fluent-error) !important; + border-bottom-color: var(--fluent-error) !important; +} + +.errorbox { + background: var(--fluent-error-light); + border: 1px solid var(--fluent-error); + border-radius: var(--fluent-radius-md); + padding: var(--fluent-space-md); + margin-bottom: var(--fluent-space-md); + color: var(--fluent-error); + font-size: var(--fluent-font-size-base); +} + +.errorbox h4 { + font-weight: var(--fluent-font-weight-semibold); + margin: 0 0 var(--fluent-space-xs); +} + +textarea, .cbi-input-textarea { + height: unset; +} + +textarea::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.cbi-input-textarea::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +textarea::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.cbi-input-textarea::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +textarea::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.cbi-input-textarea::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +textarea::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.cbi-input-textarea::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +textarea::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.cbi-input-textarea::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +textarea::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.cbi-input-textarea::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +textarea::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.cbi-input-textarea::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +textarea::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.cbi-input-textarea::-webkit-scrollbar-corner { + background: none; + display: none; +} + +textarea::-webkit-resizer { + background: none; +} + +.cbi-input-textarea::-webkit-resizer { + background: none; +} + +textarea, .cbi-input-textarea { + width: 100%; + max-width: 100%; + min-height: 80px; + padding: var(--fluent-space-sm); + resize: vertical; + box-sizing: border-box; + line-height: var(--fluent-line-height-relaxed); +} + +textarea:disabled, .cbi-input-textarea:disabled { + background: var(--fluent-input-bg-disabled); + color: var(--fluent-text-disabled); + cursor: not-allowed; + border-color: var(--fluent-border); + resize: none; + background-image: none; +} + +.textarea-wrapper { + position: relative; +} + +.textarea-wrapper textarea, .textarea-wrapper .cbi-input-textarea { + padding-bottom: calc(var(--fluent-space-xl) + var(--fluent-space-xs)); +} + +.textarea-wrapper .char-count { + bottom: var(--fluent-space-xs); + font-size: var(--fluent-font-size-xs); + color: var(--fluent-text-disabled); + position: absolute; + inset-inline-end: var(--fluent-space-sm); +} + +select { + appearance: none; + cursor: pointer; + padding-inline-end: var(--fluent-space-xl); +} + +select:after { + content: ""; +} + +select.cbi-input-select { + background-image: linear-gradient(var(--fluent-input-border-bottom-focus), var(--fluent-input-border-bottom-focus)),url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMCIgaGVpZ2h0PSI2IiB2aWV3Qm94PSIwIDAgMTAgNiIgZmlsbD0ibm9uZSI+CiAgPHBhdGggZD0iTTEgMWw0IDQgNC00IiBzdHJva2U9IiM2MTYxNjEiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiLz4KPC9zdmc+Cg==); + background-position: bottom, right 10px center; + background-repeat: no-repeat; + background-size: 0 2px, 10px 6px; + background-origin: border-box; +} + +html[dir="rtl"] select.cbi-input-select, body[data-fluent-dir="rtl"] select.cbi-input-select { + background-position: bottom, 10px; +} + +select.cbi-input-select { + transition: background-size var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + background-repeat: no-repeat; + background-origin: border-box; +} + +select.cbi-input-select:hover:not([disabled]):not(:focus) { + border-color: var(--fluent-input-border-hover); + border-bottom-color: var(--fluent-input-border-bottom-hover); +} + +select.cbi-input-select:focus { + border-bottom-color: var(--fluent-input-border-bottom-focus); + background-size: 100% 2px, 10px 6px; + outline: none; +} + +select.cbi-input-select { + appearance: none; + cursor: pointer; + box-sizing: border-box; + max-width: 100%; + padding-inline-end: var(--fluent-space-xl); +} + +select.cbi-input-select[multiple] { + height: auto; + min-height: calc(var(--fluent-input-height)*4); + padding: var(--fluent-space-xxs); + background-image: none !important; + background-position: initial !important; + background-size: initial !important; +} + +select.cbi-input-select[multiple] option { + padding: var(--fluent-space-xxs) var(--fluent-space-xs); + border-radius: var(--fluent-radius-sm); +} + +select.cbi-input-select[multiple] option:checked { + background: var(--fluent-primary-lightest); + color: var(--fluent-primary); +} + +select.cbi-input-select[multiple] option:hover { + background: var(--fluent-bg-hover); +} + +select:disabled { + background-color: var(--fluent-input-bg-disabled); + color: var(--fluent-text-disabled); + cursor: not-allowed; + border-color: var(--fluent-border); + background-image: none; +} + +.Dashboard table.table { + box-sizing: border-box; + border-collapse: collapse !important; + table-layout: auto !important; + width: 100% !important; + display: table !important; +} + +.Dashboard table.table thead, .Dashboard table.table tbody, .Dashboard table.table tfoot { + display: table-header-group !important; +} + +.Dashboard table.table tr, .Dashboard table.table .tr { + background: none; + display: table-row !important; +} + +.Dashboard table.table tr:hover, .Dashboard table.table .tr:hover { + background-color: var(--fluent-table-row-hover); +} + +.Dashboard table.table th, .Dashboard table.table .th, .Dashboard table.table td, .Dashboard table.table .td { + font-size: var(--fluent-font-size-sm) !important; + color: var(--fluent-text) !important; + border-bottom: 1px solid var(--fluent-table-border) !important; + vertical-align: middle !important; + text-align: left !important; + white-space: nowrap !important; + padding: 8px 12px !important; + display: table-cell !important; +} + +.Dashboard table.table th { + background: var(--fluent-table-header-bg) !important; + font-weight: var(--fluent-font-weight-semibold) !important; + border-bottom: 2px solid var(--fluent-table-border) !important; + color: var(--fluent-text-secondary) !important; +} + +.Dashboard table.table tfoot { + display: table-footer-group !important; +} + +.Dashboard table.table tfoot td { + font-weight: var(--fluent-font-weight-semibold) !important; + border-top: 2px solid var(--fluent-table-border) !important; + border-bottom: none !important; +} + +.cbi-value-description abbr[title] { + cursor: help; + color: var(--fluent-text-link); + border-bottom: 1px dotted var(--fluent-border-hover); + text-decoration: none; + display: inline-block; + position: relative; +} + +.cbi-value-description abbr[title]:after { + content: attr(title); + box-sizing: border-box; + width: max-content; + max-width: 280px; + padding: var(--fluent-space-xs) var(--fluent-space-sm); + border-radius: var(--fluent-radius-md); + background: var(--fluent-tooltip-bg); + color: var(--fluent-tooltip-text); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-sm); + line-height: 1.4; + font-weight: var(--fluent-font-weight-regular); + text-align: start; + box-shadow: var(--fluent-shadow-md); + opacity: 0; + visibility: hidden; + pointer-events: none; + z-index: var(--fluent-zindex-tooltip); + transition: opacity .12s var(--fluent-easing-standard),transform .12s var(--fluent-easing-standard),visibility .12s var(--fluent-easing-standard); + position: absolute; + bottom: calc(100% + 10px); + left: 50%; + transform: translateX(-50%) translateY(4px); +} + +.cbi-value-description abbr[title]:before { + content: ""; + background: var(--fluent-tooltip-bg); + opacity: 0; + visibility: hidden; + pointer-events: none; + width: 8px; + height: 8px; + z-index: calc(var(--fluent-zindex-tooltip) - 1); + transition: opacity .12s var(--fluent-easing-standard),transform .12s var(--fluent-easing-standard),visibility .12s var(--fluent-easing-standard); + position: absolute; + bottom: calc(100% + 6px); + left: 50%; + transform: translateX(-50%) translateY(4px) rotate(45deg); +} + +.cbi-value-description abbr[title]:hover:after, .cbi-value-description abbr[title]:hover:before { + opacity: 1; + visibility: visible; +} + +.cbi-value-description abbr[title]:hover:after { + transform: translateX(-50%) translateY(0); +} + +.cbi-value-description abbr[title]:hover:before { + transform: translateX(-50%) translateY(0) rotate(45deg); +} + +.cbi-tooltip-container { + align-items: center; + gap: var(--fluent-space-xs); + display: inline-flex; + position: relative; +} + +.cbi-tooltip { + top: calc(100% + var(--fluent-space-xs)); + min-width: 12rem; + max-width: min(20rem, 80vw); + padding: var(--fluent-space-sm) var(--fluent-space-md); + background: var(--fluent-bg-card); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-md); + box-shadow: var(--fluent-shadow-md); + color: var(--fluent-text); + font-family: var(--fluent-font-family); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); + opacity: 0; + transform: translateY(var(--fluent-space-xs)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard),transform var(--fluent-duration-fast) var(--fluent-easing-standard),display var(--fluent-duration-fast) var(--fluent-easing-standard) allow-discrete; + pointer-events: none; + z-index: var(--fluent-zindex-tooltip); + display: none; + position: absolute; + inset-inline-start: 0; +} + +.cbi-tooltip:before { + content: ""; + top: -4px; + background-color: var(--fluent-bg-card); + border-inline-start: 1px solid var(--fluent-border); + border-top: 1px solid var(--fluent-border); + width: 8px; + height: 8px; + position: absolute; + inset-inline-start: 30px; + transform: rotate(45deg); +} + +.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + opacity: 1; + pointer-events: auto; + display: block; + transform: translateY(0); +} + +@starting-style { + .cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + opacity: 0; + transform: translateY(var(--fluent-space-xs)); + } +} + +span[data-tooltip] { + align-items: center; + gap: var(--fluent-space-xs); + cursor: help; + color: var(--fluent-text-secondary); + display: inline-flex; + position: relative; +} + +span[data-tooltip] .label { + color: var(--fluent-text); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); + border-bottom: 1px dashed var(--fluent-border-hover); +} + +label[data-tooltip] { + align-items: center; + gap: var(--fluent-space-xs); + min-height: var(--fluent-checkbox-size); + color: var(--fluent-text); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); + cursor: help; + display: inline-flex; + position: relative; +} + +label[data-tooltip] > input[type="radio"], label[data-tooltip] > input[type="checkbox"] { + inline-size: var(--fluent-checkbox-size); + block-size: var(--fluent-checkbox-size); + flex: none; + margin: 0; +} + +label[data-tooltip]:after { + content: attr(data-tooltip); + top: calc(100% + var(--fluent-space-xs)); + min-width: 12rem; + max-width: min(20rem, 80vw); + padding: var(--fluent-space-sm) var(--fluent-space-md); + background: var(--fluent-bg-card); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-md); + box-shadow: var(--fluent-shadow-md); + color: var(--fluent-text); + font-size: var(--fluent-font-size-sm); + line-height: var(--fluent-line-height-normal); + white-space: normal; + opacity: 0; + visibility: hidden; + transform: translateY(var(--fluent-space-xs)); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard),transform var(--fluent-duration-fast) var(--fluent-easing-standard),visibility var(--fluent-duration-fast) var(--fluent-easing-standard); + pointer-events: none; + z-index: var(--fluent-zindex-tooltip); + position: absolute; + inset-inline-start: 0; +} + +label[data-tooltip]:before { + content: ""; + top: calc(100% + var(--fluent-space-xs) - 4px); + background-color: var(--fluent-bg-card); + border-inline-start: 1px solid var(--fluent-border); + border-top: 1px solid var(--fluent-border); + opacity: 0; + visibility: hidden; + width: 8px; + height: 8px; + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard),visibility var(--fluent-duration-fast) var(--fluent-easing-standard); + pointer-events: none; + z-index: calc(var(--fluent-zindex-tooltip) + 1); + position: absolute; + inset-inline-start: 30px; + transform: rotate(45deg); +} + +label[data-tooltip]:hover:after, label[data-tooltip]:focus-within:after, label[data-tooltip]:hover:before, label[data-tooltip]:focus-within:before { + opacity: 1; + visibility: visible; +} + +label[data-tooltip]:hover:after, label[data-tooltip]:focus-within:after { + transform: translateY(0); +} + +.zonebadge .cbi-tooltip { + transform: translateX(-50%) translateY(var(--fluent-space-xs)); + left: 50%; +} + +.zonebadge .cbi-tooltip:before { + left: 50%; + transform: translateX(-50%) rotate(45deg); +} + +.zonebadge:hover .cbi-tooltip:not(:empty) { + opacity: 1; + pointer-events: auto; + display: block; + transform: translateX(-50%) translateY(0); +} + +@starting-style { + .zonebadge:hover .cbi-tooltip:not(:empty) { + opacity: 0; + transform: translateX(-50%) translateY(var(--fluent-space-xs)); + } +} + +@media (min-width: 768px) { + .zonebadge .cbi-tooltip { + transform: translateY(var(--fluent-space-xs)); + inset-inline-start: 0; + } + + .zonebadge .cbi-tooltip:before { + inset-inline-start: 30px; + transform: rotate(45deg); + } + + .zonebadge:hover .cbi-tooltip:not(:empty) { + opacity: 1; + pointer-events: auto; + display: block; + transform: translateY(0); + } + + @starting-style { + .zonebadge:hover .cbi-tooltip:not(:empty) { + opacity: 0; + transform: translateY(var(--fluent-space-xs)); + } + } +} + +label[data-tooltip] { + align-items: center !important; + gap: var(--fluent-space-sm) !important; + cursor: help !important; + color: var(--fluent-text-secondary) !important; + display: inline-flex !important; +} + +:is(.cbi-section:has(.cbi-tooltip-container:hover), .controls:has(label[data-tooltip]:hover)) { + z-index: 100 !important; +} + +.fluent-menu-search { + flex-shrink: 0; + width: min(420px, 100%); + position: relative; +} + +.fluent-menu-search .fluent-menu-search-input { + border-radius: var(--fluent-radius-md); + border: 1px solid var(--fluent-border); + background: var(--fluent-bg-hover); + width: 100%; + height: 32px; + color: var(--fluent-text); + font-size: var(--fluent-font-size-base); + transition: all var(--fluent-duration-fast) var(--fluent-easing-standard); + box-sizing: border-box; + padding-block: 0; + padding-inline: 32px 52px; + box-shadow: inset 0 1px #ffffff14; +} + +.fluent-menu-search .fluent-menu-search-input:hover { + border-color: var(--fluent-input-border-hover); +} + +.fluent-menu-search .fluent-menu-search-input:focus { + border-color: var(--fluent-primary); + box-shadow: 0 0 0 1px var(--fluent-primary); + background: var(--fluent-input-bg); + outline: none; +} + +.fluent-menu-search .fluent-menu-search-input::placeholder { + color: var(--fluent-text-secondary); + opacity: .8; +} + +.fluent-menu-search .fluent-menu-search-input::-webkit-search-cancel-button { + appearance: none; + display: none; +} + +@media (max-width: 767px) { + .fluent-menu-search .fluent-menu-search-input { + padding-inline-end: var(--fluent-space-sm); + } +} + +.fluent-menu-search .fluent-menu-search-hotkey { + inset-inline-end: 8px; + background: var(--fluent-bg); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-sm); + color: var(--fluent-text-secondary); + pointer-events: none; + justify-content: center; + align-items: center; + padding: 4px 6px; + font-size: 11px; + line-height: 1; + display: flex; + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +@media (max-width: 767px) { + .fluent-menu-search .fluent-menu-search-hotkey { + display: none; + } +} + +.fluent-menu-search:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-search-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-search-20-regular); + mask-image: var(--fluent-mask-search-20-regular); + -webkit-user-select: none; + user-select: none; + top: 50%; + pointer-events: none; + z-index: 1; + display: inline-block; + position: absolute; + inset-inline-start: 10px; + transform: translateY(-50%); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +.fluent-menu-search .fluent-menu-search-overlay { + background: var(--fluent-card-bg); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-md); + box-shadow: var(--fluent-shadow-medium); + z-index: var(--fluent-zindex-popover, 1050); + max-height: 400px; + position: absolute; + overflow-y: auto; + inset-inline: 0 !important; + top: calc(100% + 4px) !important; +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.fluent-menu-search .fluent-menu-search-overlay::-webkit-resizer { + background: none; +} + +.fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list { + padding: var(--fluent-space-xs) 0; + margin: 0; + list-style: none; +} + +.fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list .fluent-menu-search-item { + padding: var(--fluent-space-xs) var(--fluent-space-sm); + cursor: pointer; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); + -webkit-user-select: none; + user-select: none; + flex-direction: column; + gap: 2px; + display: flex; +} + +.fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list .fluent-menu-search-item:hover, .fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list .fluent-menu-search-item.selected { + background: var(--fluent-sidebar-item-hover); +} + +.fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list .fluent-menu-search-item .fluent-menu-search-label { + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-regular); + font-size: 13px; +} + +.fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list .fluent-menu-search-item .fluent-menu-search-path { + color: var(--fluent-text-secondary); + font-size: 11px; +} + +.fluent-menu-search .fluent-menu-search-overlay .fluent-menu-search-list .fluent-menu-search-more { + padding: var(--fluent-space-xs) var(--fluent-space-sm); + color: var(--fluent-text-secondary); + text-align: center; + font-size: 11px; + font-style: italic; +} + +.modal:has(.fluent-menu-editor__rename-dialog) { + max-width: 480px; +} + +.fluent-menu-editor__rename-dialog { + gap: var(--fluent-space-sm); + flex-direction: column; + display: flex; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control { + position: relative; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:hover:not([disabled]):not(:focus):not(:focus-within):not(.focus):not([open]) { + border-color: var(--fluent-input-border-hover); + border-bottom-color: var(--fluent-input-border-bottom-hover); +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:focus, .fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:focus-within, .fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control.focus, .fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control[open] { + border-bottom-color: var(--fluent-input-border-bottom-focus); + outline: none; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:focus:after, .fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:focus-within:after, .fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control.focus:after, .fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control[open]:after { + transform: scaleX(1); +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control:after { + box-sizing: border-box; + content: ""; + inset-inline-start: 1px; + bottom: -1px; + background: var(--fluent-input-border-bottom-focus); + height: 2px; + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard); + pointer-events: none; + border-end-end-radius: var(--fluent-radius-sm); + border-end-start-radius: var(--fluent-radius-sm); + position: absolute; + inset-inline-end: 1px; + transform: scaleX(0); +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control { + align-items: stretch; + gap: 0; + min-width: 0; + padding: 0; + display: flex; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control > .cbi-input-text { + min-width: 0; + height: 100%; + padding: 0 var(--fluent-space-sm); + color: var(--fluent-text); + box-shadow: none; + background: none; + border: 0; + outline: none; + flex: auto; + margin: 0; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control > .fluent-menu-editor__item-reset { + flex: 0 0 var(--fluent-input-height); + width: var(--fluent-input-height); + height: 100%; + box-shadow: none; + background: none; + border: 0; + border-start-start-radius: 0; + border-end-start-radius: 0; + margin: 0; +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control > .fluent-menu-editor__item-reset:hover { + background: var(--fluent-bg-hover); +} + +.fluent-menu-editor__rename-dialog > .fluent-menu-editor__rename-control > .fluent-menu-editor__item-reset:active { + background: var(--fluent-bg-active); +} + +.fluent-menu-editor__rename-dialog > .right { + justify-content: flex-end; + gap: var(--fluent-space-sm); + margin-top: var(--fluent-space-xs); + display: flex; +} + +.fluent-menu-editor { + gap: var(--fluent-space-md); + flex-direction: column; + width: 100%; + display: flex; +} + +.fluent-menu-editor__actions { + justify-content: flex-end; + gap: var(--fluent-space-sm); + flex-wrap: wrap; + display: flex; +} + +.fluent-menu-editor__notice, .fluent-menu-editor__validation { + padding: var(--fluent-space-sm) var(--fluent-space-md); + color: var(--fluent-error); + background: var(--fluent-error-light); + border: 1px solid var(--fluent-error); + border-radius: var(--fluent-radius-md); +} + +.fluent-menu-editor__validation:empty { + display: none; +} + +.fluent-menu-editor__categories { + align-items: start; + gap: var(--fluent-space-md); + grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr)); + display: grid; +} + +.fluent-menu-editor__category { + --fluent-menu-drop-offset: calc(var(--fluent-space-sm) * -1); + min-width: 0; + padding: var(--fluent-space-sm); + background: var(--fluent-bg-card); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-lg); + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard),opacity var(--fluent-duration-fast) var(--fluent-easing-standard); + position: relative; +} + +.fluent-menu-editor__category.is-hidden { + opacity: .55; +} + +.fluent-menu-editor__category.is-invalid { + border-color: var(--fluent-error); +} + +.fluent-menu-editor__category.is-dragging { + opacity: .55; +} + +.fluent-menu-editor__category.is-drag-over { + border-color: var(--fluent-primary); + background: var(--fluent-primary-lightest); +} + +.fluent-menu-editor__category-header { + align-items: center; + gap: var(--fluent-space-xs); + cursor: pointer; + min-width: 0; + list-style: none; + display: flex; +} + +.fluent-menu-editor__category-header::-webkit-details-marker { + display: none; +} + +.fluent-menu-editor__category-header:after { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-chevron-right-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-chevron-right-20-regular); + mask-image: var(--fluent-mask-chevron-right-20-regular); + -webkit-user-select: none; + user-select: none; + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard); + margin-inline-start: auto; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +html[dir="rtl"] .fluent-menu-editor__category-header:after, body[data-fluent-dir="rtl"] .fluent-menu-editor__category-header:after { + transform: rotate(180deg); +} + +.fluent-menu-editor__category[open] .fluent-menu-editor__category-header:after { + transform: rotate(90deg); +} + +.fluent-menu-editor__category-name { + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; + min-width: 0; + overflow: hidden; +} + +.fluent-menu-editor__category-count { + min-width: var(--fluent-space-xl); + height: var(--fluent-space-xl); + padding-inline: var(--fluent-space-xs); + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-sm); + background: var(--fluent-bg-hover); + border-radius: var(--fluent-radius-circular); + justify-content: center; + align-items: center; + display: inline-flex; +} + +.fluent-menu-editor__category-delete, .fluent-menu-editor__category-edit, .fluent-menu-editor__item-reset { + flex: 0 0 var(--fluent-space-xxl); + width: var(--fluent-space-xxl); + height: var(--fluent-space-xxl); + color: var(--fluent-text-secondary); + cursor: pointer; + border-radius: var(--fluent-radius-md); + background: none; + border: 0; + justify-content: center; + align-items: center; + padding: 0; + display: inline-flex; +} + +.fluent-menu-editor__category-delete:hover, .fluent-menu-editor__category-edit:hover, .fluent-menu-editor__item-reset:hover { + color: var(--fluent-text); + background: var(--fluent-bg-hover); +} + +.fluent-menu-editor__category-delete:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-delete-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-delete-20-regular); + mask-image: var(--fluent-mask-delete-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-menu-editor__category-delete:hover { + color: var(--fluent-error); + background: var(--fluent-error-light); +} + +.fluent-menu-editor__category-error { + margin-top: var(--fluent-space-xs); + color: var(--fluent-error); + font-size: var(--fluent-font-size-sm); +} + +.fluent-menu-editor__category-error:empty { + display: none; +} + +.fluent-menu-editor__category.is-drop-before:after, .fluent-menu-editor__category.is-drop-after:after, .fluent-menu-editor__item.is-drop-before:after, .fluent-menu-editor__item.is-drop-after:after { + z-index: 1; + inset-inline: var(--fluent-space-xs); + height: var(--fluent-space-xxs); + pointer-events: none; + content: ""; + background: var(--fluent-primary); + border-radius: var(--fluent-radius-circular); + position: absolute; +} + +.fluent-menu-editor__category.is-drop-before:after, .fluent-menu-editor__item.is-drop-before:after { + top: var(--fluent-menu-drop-offset); +} + +.fluent-menu-editor__category.is-drop-after:after, .fluent-menu-editor__item.is-drop-after:after { + bottom: var(--fluent-menu-drop-offset); +} + +.fluent-menu-editor__drop-placeholder { + box-sizing: border-box; + pointer-events: none; + background: var(--fluent-primary-lightest); + border: 1px dashed var(--fluent-primary); + min-width: 0; +} + +.fluent-menu-editor__drop-placeholder--category { + border-radius: var(--fluent-radius-lg); +} + +.fluent-menu-editor__drop-placeholder--item { + border-radius: var(--fluent-radius-md); +} + +.fluent-menu-editor__drag-handle { + width: 24px; + height: 32px; + color: var(--fluent-text-secondary); + cursor: grab; + -webkit-user-select: none; + user-select: none; + touch-action: none; + justify-content: center; + align-items: center; + display: inline-flex; +} + +.fluent-menu-editor__drag-handle:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-re-order-dots-vertical-20-filled: url("data:image/svg+xml,"); + width: 20px; + height: 20px; + -webkit-mask-image: var(--fluent-mask-fluent-re-order-dots-vertical-20-filled); + mask-image: var(--fluent-mask-fluent-re-order-dots-vertical-20-filled); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-menu-editor__drag-handle:active { + cursor: grabbing; +} + +.fluent-menu-editor__drag-preview { + z-index: var(--fluent-zindex-tooltip); + pointer-events: none; + opacity: .9; + box-shadow: var(--fluent-shadow-medium); + will-change: left,top; + position: fixed; +} + +.fluent-menu-editor__item.fluent-menu-editor__drag-preview { + position: fixed; +} + +.fluent-menu-editor__items { + align-items: start; + gap: var(--fluent-space-xs); + margin-top: var(--fluent-space-sm); + min-height: 44px; + max-height: 55vh; + padding: var(--fluent-space-xs); + border-radius: var(--fluent-radius-md); + border: 1px dashed #0000; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 240px), 1fr)); + display: grid; + overflow-y: auto; +} + +.fluent-menu-editor__items.is-drag-over { + border-color: var(--fluent-primary); + background: var(--fluent-primary-lightest); +} + +.fluent-menu-editor__items:has( > .fluent-menu-editor__drop-placeholder) > .fluent-menu-editor__empty { + display: none; +} + +.fluent-menu-editor__item { + --fluent-menu-drop-offset: calc(var(--fluent-space-xs) * -1); + grid-template-columns: auto auto minmax(0, 1fr) var(--fluent-space-xxl) auto; + align-items: center; + gap: var(--fluent-space-sm); + min-width: 0; + padding: var(--fluent-space-xs) var(--fluent-space-sm); + background: var(--fluent-bg); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-md); + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard); + display: grid; + position: relative; +} + +.fluent-menu-editor__item > .fluent-menu-editor__item-reset { + grid-column: 4; +} + +.fluent-menu-editor__item > input[type="checkbox"] { + grid-column: 5; +} + +.fluent-menu-editor__item.is-hidden { + opacity: .55; +} + +.fluent-menu-editor__item.is-dragging { + opacity: .35; +} + +.fluent-menu-editor__category.is-drag-source, .fluent-menu-editor__item.is-drag-source { + opacity: 0; + pointer-events: none; + transition: none; + position: absolute; +} + +.fluent-menu-editor__category-edit:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-edit-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-edit-20-regular); + mask-image: var(--fluent-mask-edit-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-menu-editor__item-reset:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-arrow-clockwise-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-arrow-clockwise-20-regular); + mask-image: var(--fluent-mask-arrow-clockwise-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-menu-editor__item-label { + min-width: 0; + color: var(--fluent-text); + flex-direction: column; + display: flex; +} + +.fluent-menu-editor__item-label strong, .fluent-menu-editor__item-label small { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.fluent-menu-editor__item-label small { + color: var(--fluent-text-secondary); +} + +.fluent-menu-editor__empty { + min-height: 44px; + color: var(--fluent-text-secondary); + border: 1px dashed var(--fluent-border); + border-radius: var(--fluent-radius-md); + justify-content: center; + align-items: center; + display: flex; +} + +.fluent-about-manager { + max-width: 600px; + padding: var(--fluent-card-padding); + background-color: var(--fluent-bg-card); + border-radius: var(--fluent-radius-md); + flex-direction: column; + gap: 16px; + margin: 0 auto; + display: flex; +} + +.fluent-about-manager h3 { + color: var(--fluent-text); + margin: 8px 0 4px; + font-size: 16px; + font-weight: 600; +} + +.fluent-about-logo { + text-align: center; + flex-direction: column; + align-items: center; + padding: 12px 0; + display: flex; +} + +.fluent-about-logo img { + width: 64px; + height: 64px; + margin-bottom: 12px; +} + +.fluent-about-logo h2 { + color: var(--fluent-text); + margin: 0; + font-size: 20px; + font-weight: 600; +} + +.fluent-about-logo .fluent-about-subtitle { + color: var(--fluent-text-secondary); + margin: 4px 0 0; + font-size: 13px; +} + +.fluent-about-details { + flex-direction: column; + gap: 8px; + padding: 8px 0; + display: flex; +} + +.fluent-about-details .fluent-about-detail-row { + color: var(--fluent-text); + justify-content: space-between; + font-size: 13px; + display: flex; +} + +.fluent-about-details .fluent-about-detail-row strong { + color: var(--fluent-text-secondary); + font-weight: 500; +} + +.fluent-about-details .fluent-about-detail-row a { + color: var(--fluent-primary); + text-decoration: none; +} + +.fluent-about-details .fluent-about-detail-row a:hover { + text-decoration: underline; +} + +.fluent-about-divider { + border: none; + border-top: 1px solid var(--fluent-border); + margin: 8px 0; +} + +.fluent-update-controls { + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 16px; + display: flex; +} + +.fluent-update-controls .fluent-update-channel-selector { + color: var(--fluent-text); + align-items: center; + gap: 8px; + font-size: 13px; + display: flex; +} + +.fluent-update-controls .fluent-update-channel-selector label { + color: var(--fluent-text-secondary); +} + +.fluent-update-controls .fluent-update-channel-selector select { + min-width: 140px; + padding: 4px 8px; +} + +.fluent-update-status { + border-radius: var(--fluent-radius-sm); + background-color: var(--fluent-bg); + color: var(--fluent-text); + border-left: 3px solid #0000; + padding: 8px 12px; + font-size: 13px; +} + +.fluent-update-status.status-info { + border-left-color: var(--fluent-primary); +} + +.fluent-update-status.status-success { + background-color: #107c410d; + border-left-color: #107c41; +} + +.fluent-update-status.status-warn { + background-color: #ffb9000d; + border-left-color: #ffb900; +} + +.fluent-update-status.status-error { + color: #a80000; + background-color: #a800000d; + border-left-color: #a80000; +} + +.fluent-update-card { + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-sm); + background-color: var(--fluent-bg); + flex-direction: column; + gap: 12px; + padding: 12px; + display: flex; +} + +.fluent-update-card .fluent-update-header { + justify-content: space-between; + align-items: center; + display: flex; +} + +.fluent-update-card .fluent-update-header .fluent-update-badge { + background-color: var(--fluent-primary); + color: #fff; + border-radius: 12px; + padding: 2px 8px; + font-size: 11px; + font-weight: 600; +} + +.fluent-update-card .fluent-update-header .fluent-update-date { + color: var(--fluent-text-secondary); + font-size: 12px; +} + +.fluent-update-card .fluent-update-changelog { + color: var(--fluent-text); + background-color: var(--fluent-bg-card); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-sm); + white-space: pre-wrap; + max-height: 120px; + margin: 0; + padding: 8px; + font-family: inherit; + font-size: 12px; + overflow-y: auto; +} + +.fluent-update-card .fluent-update-footer { + justify-content: flex-end; + display: flex; +} + +.fluent-update-card .fluent-update-success { + text-align: center; + padding: 8px 0; +} + +.fluent-update-card .fluent-update-success h4 { + color: #107c41; + margin: 0 0 8px; + font-weight: 600; +} + +.fluent-update-card .fluent-update-success p { + color: var(--fluent-text-secondary); + margin: 0; + font-size: 13px; +} + +.fluent-progress-bar { + background-color: var(--fluent-border); + border-radius: 2px; + width: 100%; + height: 4px; + position: relative; + overflow: hidden; +} + +.fluent-progress-bar__fill { + background-color: var(--fluent-primary); + border-radius: 2px; + height: 100%; + transition: width .2s ease-in-out; +} + +.fluent-progress-bar__fill.fill-verify, .fluent-progress-bar__fill.fill-install { + transform-origin: 0; + animation: 1.5s linear infinite fluent-indeterminate-progress; + width: 100% !important; +} + +.fluent-progress-text { + color: var(--fluent-text-secondary); + text-align: center; + margin-top: -8px; + font-size: 12px; +} + +@keyframes fluent-indeterminate-progress { + 0% { + transform: translateX(-100%) scaleX(.2); + } + + 50% { + transform: translateX(-20%) scaleX(.5); + } + + 100% { + transform: translateX(100%) scaleX(.2); + } +} + +.fluent-mac-selector-group { + align-items: center; + gap: var(--fluent-space-xs); + box-sizing: border-box; + width: 100%; + max-width: 100%; + display: flex; +} + +.fluent-mac-selector-group > input.cbi-input-text { + flex: auto !important; + width: 100% !important; + min-width: 160px !important; +} + +.fluent-mac-selector-group > .fluent-mac-edit-btn { + border-radius: var(--fluent-radius-md); + flex-shrink: 0; + justify-content: center; + align-items: center; + display: inline-flex; + width: var(--fluent-input-height) !important; + height: var(--fluent-input-height) !important; + min-width: var(--fluent-input-height) !important; + padding: 0 !important; +} + +.fluent-mac-selector-group > .fluent-mac-edit-btn:after { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-edit-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-edit-20-regular); + mask-image: var(--fluent-mask-fluent-edit-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-mac-overlay-backdrop { + z-index: 2500; + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + padding: var(--fluent-space-lg); + background: #0006; + justify-content: center; + align-items: center; + animation: .2s cubic-bezier(0, 0, .2, 1) fluent-mac-fade-in; + display: flex; + position: fixed; + inset: 0; +} + +@keyframes fluent-mac-fade-in { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.fluent-mac-overlay-card { + background: var(--fluent-bg-card); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-lg); + box-shadow: var(--fluent-shadow-lg, 0 12px 32px #0000004d); + flex-direction: column; + width: min(90vw, 440px); + max-width: 100%; + animation: .2s cubic-bezier(0, 0, .2, 1) fluent-mac-pop-in; + display: flex; + position: relative; + overflow: visible !important; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header { + padding: var(--fluent-space-lg) var(--fluent-space-xl); + border-bottom: 1px solid var(--fluent-border); + border-top-left-radius: inherit; + border-top-right-radius: inherit; + justify-content: space-between; + align-items: center; + display: flex; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header h3 { + font-size: var(--fluent-font-size-md); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + margin: 0; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header .fluent-mac-overlay-close { + width: 32px; + height: 32px; + color: var(--fluent-text-secondary); + cursor: pointer; + background: none; + border: none; + justify-content: center; + align-items: center; + padding: 0; + display: inline-flex; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header .fluent-mac-overlay-close:after { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-dismiss-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-dismiss-20-regular); + mask-image: var(--fluent-mask-fluent-dismiss-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-header .fluent-mac-overlay-close:hover { + background: var(--fluent-bg-hover); + color: var(--fluent-text); +} + +.fluent-mac-overlay-card .fluent-mac-overlay-body { + padding: var(--fluent-space-xl); + gap: var(--fluent-space-lg); + flex-direction: column; + display: flex; + overflow: visible !important; +} + +.fluent-mac-overlay-card .fluent-mac-field-block { + gap: var(--fluent-space-xs); + flex-direction: column; + display: flex; + position: relative; + overflow: visible !important; +} + +.fluent-mac-overlay-card .fluent-mac-field-block .fluent-mac-label { + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text-secondary); +} + +.fluent-mac-overlay-card .fluent-mac-field-block .fluent-mac-modal-input { + width: 100%; + font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + font-size: var(--fluent-font-size-md); + font-weight: var(--fluent-font-weight-semibold); + text-align: center; + letter-spacing: 1px; + padding: var(--fluent-space-sm); +} + +.fluent-mac-overlay-card .fluent-mac-picker-row { + align-items: center; + gap: var(--fluent-space-sm); + width: 100%; + display: flex; + overflow: visible !important; +} + +.fluent-mac-overlay-card .fluent-mac-picker-row .fluent-mac-random-btn { + white-space: nowrap; + align-items: center; + gap: var(--fluent-space-xs); + flex-shrink: 0; + display: inline-flex; +} + +.fluent-mac-overlay-card .fluent-mac-picker-row .fluent-mac-random-btn:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-arrow-clockwise-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-arrow-clockwise-20-regular); + mask-image: var(--fluent-mask-fluent-arrow-clockwise-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +.fluent-mac-overlay-card .fluent-mac-vendor-picker-wrap { + flex: 1; + min-width: 0; + position: relative; + overflow: visible !important; +} + +.fluent-mac-overlay-card .fluent-mac-overlay-footer { + justify-content: flex-end; + align-items: center; + gap: var(--fluent-space-sm); + padding: var(--fluent-space-md) var(--fluent-space-xl); + border-top: 1px solid var(--fluent-border); + background: var(--fluent-bg-secondary); + border-bottom-left-radius: inherit; + border-bottom-right-radius: inherit; + display: flex; +} + +@keyframes fluent-mac-pop-in { + from { + opacity: 0; + transform: scale(.95) translateY(-10px); + } + + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +img[src="/luci-static/resources/icons/port_down.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iNzUgNzUgMzYyIDM2MiIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InRpdGxlIGRlc2MiPgogIDx0aXRsZSBpZD0idGl0bGUiPkV0aGVybmV0IHBvcnQgd2l0aCBkaXNjb25uZWN0ZWQgcGx1ZzwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgRmx1ZW50IFVJIHN0eWxlIGV0aGVybmV0IHBvcnQgaWNvbiB3aXRoIGVpZ2h0IFJKNDUgY29udGFjdHMgYW5kIGEgcGx1ZyBkaXNjb25uZWN0ZWQgc3ltYm9sLjwvZGVzYz4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiM0MjQyNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMzMzIDEzNUgxMzljLTguOCAwLTE2IDcuMi0xNiAxNnYyMjFjMCA4LjggNy4yIDE2IDE2IDE2aDIzOWM4LjggMCAxNi03LjIgMTYtMTZWMjA2IiBzdHJva2Utd2lkdGg9IjEwIi8+CiAgICA8cGF0aCBkPSJNMTcxIDIwOWgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yODUgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMjExdjI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICA8L2c+CiAgPHBhdGggZmlsbD0iIzQyNDI0MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzEyIDg2KSBzY2FsZSg2LjYpIiBkPSJNMTcuODU0IDIuMTQ2YS41LjUgMCAwIDEgMCAuNzA4bC0xLjY2OCAxLjY2OGE0LjA0IDQuMDQgMCAwIDEtLjMzMiA1LjMzMWwtLjMuM2wtLjAwMy4wMDNsLS4zMDkuMzA5YTEuMDUgMS4wNSAwIDAgMS0xLjQ4NCAwTDkuNTM1IDYuMjQyYTEuMDUgMS4wNSAwIDAgMSAwLTEuNDg1bC42MTEtLjYxYTQuMDM2IDQuMDM2IDAgMCAxIDUuMzMyLS4zMzJsMS42NjgtMS42NjlhLjUuNSAwIDAgMSAuNzA4IDBNMTUuMTQgNC44NDdhMy4wMzcgMy4wMzcgMCAwIDAtNC4yODYuMDA2bC0uMy4zYS40OS40OSAwIDAgMCAwIC42OTNsMy42IDMuNmMuMTkuMTkxLjUuMTkyLjY5MS4wMDJsLjAwMi0uMDAybC4zLS4zYTMuMDM2IDMuMDM2IDAgMCAwIC4wMDYtNC4yODZsLS4wMDctLjAwNnptLTUuNzg2IDMuOGEuNS41IDAgMCAxIDAgLjcwN0w3LjcwNyAxMUw5IDEyLjI5M2wxLjY0Ni0xLjY0N2EuNS41IDAgMCAxIC43MDguNzA4TDkuNzA3IDEzbC40NDcuNDQ3YTEuNDkgMS40OSAwIDAgMSAwIDIuMTA3bC0uMy4zYTQuMDM2IDQuMDM2IDAgMCAxLTUuMzMyLjMzMmwtMS42NjggMS42NjhhLjUuNSAwIDAgMS0uNzA4LS43MDhsMS42NjktMS42NjhhNC4wMzYgNC4wMzYgMCAwIDEgLjMzMS01LjMzMWwuMy0uM2ExLjQ5IDEuNDkgMCAwIDEgMi4xMDggMGwuNDQ2LjQ0NmwxLjY0Ni0xLjY0N2EuNS41IDAgMCAxIC43MDggMG0tNC40OTUgNi41MDVhMy4wMzYgMy4wMzYgMCAwIDAgNC4yODctLjAwNWwuMy0uM2EuNDkuNDkgMCAwIDAgMC0uNjkzbC0zLjYtMy42YS40OS40OSAwIDAgMC0uNjkyIDBsLS4zLjNhMy4wMzYgMy4wMzYgMCAwIDAgMCA0LjI5MnoiLz4KPC9zdmc+Cg==); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/port_up.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iNzUgNzUgMzYyIDM2MiIgcm9sZT0iaW1nIiBhcmlhLWxhYmVsbGVkYnk9InRpdGxlIGRlc2MiPgogIDx0aXRsZSBpZD0idGl0bGUiPkV0aGVybmV0IHBvcnQgd2l0aCBjb25uZWN0ZWQgcGx1ZzwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgRmx1ZW50IFVJIHN0eWxlIGV0aGVybmV0IHBvcnQgaWNvbiB3aXRoIGVpZ2h0IFJKNDUgY29udGFjdHMgYW5kIGEgcGx1ZyBjb25uZWN0ZWQgY2hlY2ttYXJrIHN5bWJvbC48L2Rlc2M+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDI0MjQyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTMzMyAxMzVIMTM5Yy04LjggMC0xNiA3LjItMTYgMTZ2MjIxYzAgOC44IDcuMiAxNiAxNiAxNmgyMzljOC44IDAgMTYtNy4yIDE2LTE2VjIwNiIgc3Ryb2tlLXdpZHRoPSIxMCIvPgogICAgPHBhdGggZD0iTTE3MSAyMDloMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjEwIi8+CiAgICA8cGF0aCBkPSJNMTk1IDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjEzIDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjMxIDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjQ5IDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjY3IDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjg1IDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMzAzIDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMzIxIDIxMXYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgPC9nPgogIDxwYXRoIGZpbGw9IiM0MjQyNDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMxMiA4Nikgc2NhbGUoNi42KSIgZD0iTTE3Ljg1NCAyLjg1NGEuNS41IDAgMCAwLS43MDgtLjcwOGwtMi42NjggMi42NjlhNC4wMzYgNC4wMzYgMCAwIDAtNS4zMzIuMzMxbC0uMy4zYTEuNDkgMS40OSAwIDAgMCAwIDIuMTA4bDIuNDY1IDIuNDY0cS40MjEtLjMuODk1LS41MThMOS41NTMgNi44NDZhLjQ5LjQ5IDAgMCAxIDAtLjY5MmwuMy0uM2EzLjAzNiAzLjAzNiAwIDAgMSA1LjAxIDMuMTU4cS41MTQuMDM0IDEgLjE1OGE0LjA0IDQuMDQgMCAwIDAtLjY3OC0zLjY0OHptLTEwLjMgNS45OTJsMi40NjQgMi40NjVxLS4zLjQyMS0uNTE4Ljg5NUw2Ljg0NyA5LjU1NGEuNDkuNDkgMCAwIDAtLjY5MyAwbC0uMy4zYTMuMDM1IDMuMDM1IDAgMCAwIDMuMTU4IDUuMDA5cS4wMzQuNTE1LjE1OCAxYTQuMDQgNC4wNCAwIDAgMS0zLjY0OC0uNjc4bC0yLjY2OCAyLjY2OWEuNS41IDAgMCAxLS43MDgtLjcwOGwyLjY2OS0yLjY2OGE0LjAzNiA0LjAzNiAwIDAgMSAuMzMyLTUuMzMybC4zLS4zYTEuNDkgMS40OSAwIDAgMSAyLjEwNyAwTTE5IDE0LjVhNC41IDQuNSAwIDEgMS05IDBhNC41IDQuNSAwIDAgMSA5IDBtLTIuMTQ2LTEuODU0YS41LjUgMCAwIDAtLjcwOCAwTDEzLjUgMTUuMjkzbC0uNjQ2LS42NDdhLjUuNSAwIDAgMC0uNzA4LjcwOGwxIDFhLjUuNSAwIDAgMCAuNzA4IDBsMy0zYS41LjUgMCAwIDAgMC0uNzA4Ii8+Cjwvc3ZnPgo=); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-primary) !important; +} + +img[src="/luci-static/resources/icons/ethernet.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMTE1IDgwIDMzNCAzMzQiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5FdGhlcm5ldCBwb3J0PC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzYyI+QSBGbHVlbnQgVUkgc3R5bGUgZXRoZXJuZXQgcG9ydCBpY29uIHdpdGggZWlnaHQgUko0NSBjb250YWN0cy48L2Rlc2M+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDI0MjQyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYyMjFjMCA4LjgtNy4yIDE2LTE2IDE2SDEzOWMtOC44IDAtMTYtNy4yLTE2LTE2VjE1MWMwLTguOCA3LjItMTYgMTYtMTZ6IiBzdHJva2Utd2lkdGg9IjEwIi8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICA8L2c+Cjwvc3ZnPgo=); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/ethernet_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMTE1IDgwIDMzNCAzMzQiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5OZXR3b3JrIHR1bm5lbCAoZGlzYWJsZWQpPC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzYyI+QSBGbHVlbnQgVUkgc3R5bGUgZXRoZXJuZXQgcG9ydCBpY29uIGluIGEgZGlzYWJsZWQgc3RhdGUsIHNob3dpbmcgYW4gb2ZmbGluZSBiYWRnZS48L2Rlc2M+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDI0MjQyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPCEtLSBDdXRvdXQgb3V0ZXIgYm94IC0tPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYyMjFjMCA4LjgtNy4yIDE2LTE2IDE2SDIwNSBNMTIzIDMyNFYxNTFjMC04LjggNy4yLTE2IDE2LTE2IiBzdHJva2Utd2lkdGg9IjEwIi8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICA8L2c+CiAgPCEtLSBPZmZsaW5lIEJhZGdlIChCb3R0b20tTGVmdCkgLS0+CiAgPHBhdGggZmlsbD0iIzQyNDI0MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIzIDMyNCkgc2NhbGUoOCkiIGQ9Ik02Ljg1NCAzLjE0NmEuNS41IDAgMCAxIDAgLjcwOEw1LjcwNyA1bDEuMTQ3IDEuMTQ2YS41LjUgMCAxIDEtLjcwOC43MDhMNSA1LjcwN0wzLjg1NCA2Ljg1NGEuNS41IDAgMSAxLS43MDgtLjcwOEw0LjI5MyA1TDMuMTQ2IDMuODU0YS41LjUgMCAxIDEgLjcwOC0uNzA4TDUgNC4yOTNsMS4xNDYtMS4xNDdhLjUuNSAwIDAgMSAuNzA4IDBNMCA1YTUgNSAwIDEgMSAxMCAwQTUgNSAwIDAgMSAwIDVtNS00YTQgNCAwIDEgMCAwIDhhNCA0IDAgMCAwIDAtOCIvPgo8L3N2Zz4K); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/bridge.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+TmV0d29yayBicmlkZ2U8L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjIj5BIEZsdWVudCBVSSBzdHlsZSBuZXR3b3JrIGJyaWRnZSBpY29uIHNob3dpbmcgdHdvIGJyaWRnZWQgZXRoZXJuZXQgcG9ydHMuPC9kZXNjPgogIAogIDwhLS0gQ29ubmVjdGlvbiBMLWxpbmUgZG90cyAtLT4KICA8ZyBmaWxsPSIjMDA3OEQ0Ij4KICAgICAgPGNpcmNsZSBjeD0iMjYiIGN5PSIxMy41IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjMwIiBjeT0iMTMuNSIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIzNCIgY3k9IjEzLjUiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMzQiIGN5PSIxNy41IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjM0IiBjeT0iMjEuNSIgcj0iMS41IiAvPgogIDwvZz4KCiAgPCEtLSBQb3J0IDEgKFRvcC1MZWZ0KSAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0LCA0KSBzY2FsZSgwLjA3NCkgdHJhbnNsYXRlKC0xMjMsIC0xMzUpIiBzdHJva2U9IiMwMDc4RDQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djIyMWMwIDguOC03LjIgMTYtMTYgMTZIMTM5Yy04LjggMC0xNi03LjItMTYtMTZWMTUxYzAtOC44IDcuMi0xNiAxNi0xNnoiIHN0cm9rZS13aWR0aD0iMC43NSIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjAuNiIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE5NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjEzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI0OSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjY3IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMwMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzIxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICA8L2c+CgogIDwhLS0gUG9ydCAyIChCb3R0b20tUmlnaHQpIC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI0LCAyNSkgc2NhbGUoMC4wNzQpIHRyYW5zbGF0ZSgtMTIzLCAtMTM1KSIgc3Ryb2tlPSIjMDA3OEQ0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYyMjFjMCA4LjgtNy4yIDE2LTE2IDE2SDEzOWMtOC44IDAtMTYtNy4yLTE2LTE2VjE1MWMwLTguOCA3LjItMTYgMTYtMTZ6IiBzdHJva2Utd2lkdGg9IjAuNzUiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xNzEgMTg2aDE3M3YxMDFoLTMzdjI3aC0yN3YyM2gtNTV2LTIzaC0yN3YtMjdoLTMxeiIgc3Ryb2tlLXdpZHRoPSIwLjYiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIxMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjMxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI2NyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjg1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMyMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgPC9nPgo8L3N2Zz4K); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/bridge_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+TmV0d29yayBicmlkZ2UgKGRpc2FibGVkKTwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgRmx1ZW50IFVJIHN0eWxlIG5ldHdvcmsgYnJpZGdlIGljb24gc2hvd2luZyB0d28gYnJpZGdlZCBldGhlcm5ldCBwb3J0cyBpbiBhIGRpc2FibGVkIHN0YXRlLjwvZGVzYz4KICAKICA8IS0tIENvbm5lY3Rpb24gTC1saW5lIGRvdHMgLS0+CiAgPGcgZmlsbD0iIzAwNzhENCI+CiAgICAgIDxjaXJjbGUgY3g9IjI2IiBjeT0iMTMuNSIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIzMCIgY3k9IjEzLjUiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMzQiIGN5PSIxMy41IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjM0IiBjeT0iMTcuNSIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIzNCIgY3k9IjIxLjUiIHI9IjEuNSIgLz4KICA8L2c+CgogIDwhLS0gT2ZmbGluZSBCYWRnZSAoQm90dG9tLUxlZnQpIC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMsIDI5KSBzY2FsZSgxLjYpIiBmaWxsPSIjMDA3OEQ0Ij4KICAgICAgPHBhdGggZD0iTTYuODU0IDMuMTQ2YS41LjUgMCAwIDEgMCAuNzA4TDUuNzA3IDVsMS4xNDcgMS4xNDZhLjUuNSAwIDEgMS0uNzA4LjcwOEw1IDUuNzA3TDMuODU0IDYuODU0YS41LjUgMCAxIDEtLjcwOC0uNzA4TDQuMjkzIDVMMy4xNDYgMy44NTRhLjUuNSAwIDEgMSAuNzA4LS43MDhMNSA0LjI5M2wxLjE0Ni0xLjE0N2EuNS41IDAgMCAxIC43MDggME0wIDVhNSA1IDAgMSAxIDEwIDBBNSA1IDAgMCAxIDAgNW01LTRhNCA0IDAgMSAwIDAgOGE0IDQgMCAwIDAgMC04Ii8+CiAgPC9nPgoKICA8IS0tIFBvcnQgMSAoVG9wLUxlZnQpIC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQsIDQpIHNjYWxlKDAuMDc0KSB0cmFuc2xhdGUoLTEyMywgLTEzNSkiIHN0cm9rZT0iIzAwNzhENCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDxwYXRoIGQ9Ik0xMzkgMTM1aDIzOWM4LjggMCAxNiA3LjIgMTYgMTZ2MjIxYzAgOC44LTcuMiAxNi0xNiAxNkgxMzljLTguOCAwLTE2LTcuMi0xNi0xNlYxNTFjMC04LjggNy4yLTE2IDE2LTE2eiIgc3Ryb2tlLXdpZHRoPSIwLjc1IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMC42IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTk1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIzMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjQ5IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI4NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzAzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogIDwvZz4KCiAgPCEtLSBQb3J0IDIgKEJvdHRvbS1SaWdodCkgLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQsIDI1KSBzY2FsZSgwLjA3NCkgdHJhbnNsYXRlKC0xMjMsIC0xMzUpIiBzdHJva2U9IiMwMDc4RDQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djIyMWMwIDguOC03LjIgMTYtMTYgMTZIMTM5Yy04LjggMC0xNi03LjItMTYtMTZWMTUxYzAtOC44IDcuMi0xNiAxNi0xNnoiIHN0cm9rZS13aWR0aD0iMC43NSIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjAuNiIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE5NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjEzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI0OSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjY3IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMwMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzIxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICA8L2c+Cjwvc3ZnPgo=); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/tunnel.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+TmV0d29yayB0dW5uZWw8L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjIj5BIEZsdWVudCBVSSBzdHlsZSBuZXR3b3JrIHR1bm5lbCBpY29uIHNob3dpbmcgdHdvIGV0aGVybmV0IHBvcnRzIGNvbm5lY3RlZCB2aWEgYSBnbG9iYWwgdHVubmVsLjwvZGVzYz4KICAKICA8IS0tIENvbm5lY3Rpb24gTC1saW5lIGRvdHMgLS0+CiAgPGcgZmlsbD0iIzAwNzhENCI+CiAgICAgIDxjaXJjbGUgY3g9IjI1IiBjeT0iMTMuNSIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIzNCIgY3k9IjIyLjc1IiByPSIxLjUiIC8+CiAgPC9nPgoKICA8IS0tIENvbm5lY3Rpb24gRWFydGggSWNvbiAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNS42LCA1LjEpIHNjYWxlKDEuNCkiIGZpbGw9IiMwMDc4RDQiPgogICAgICA8cGF0aCBkPSJNMTEgNkE1IDUgMCAxIDAgMSA2YTUgNSAwIDAgMCAxMCAwbS02Ljk5LjVjLjA1MiAxLjMwNy4zMDUgMi40NjcuNjgxIDMuMjgxQTQgNCAwIDAgMSAyLjAzMSA2LjV6bTEgMGgxLjk4Yy0uMDQ3IDEuMDc1LS4yNCAyLjAwNS0uNTA0IDIuNjY0Yy0uMTUzLjM4My0uMzEuNjI1LS40MzQuNzU2TDYgOS45N2wtLjA1Mi0uMDVjLS4xMjMtLjEzLS4yOC0uMzczLS40MzQtLjc1NmMtLjI2NC0uNjYtLjQ1Ny0xLjU4OS0uNTAzLTIuNjY0bTEuOTgtMUg1LjAxYy4wNDctMS4wNzUuMjQtMi4wMDUuNTA0LTIuNjY0Yy4xNTMtLjM4My4zMS0uNjI1LjQzNC0uNzU2TDYgMi4wM2wuMDUyLjA1Yy4xMjMuMTMuMjguMzczLjQzNC43NTZjLjI2NC42Ni40NTcgMS41ODkuNTAzIDIuNjY0bTEgMUg5Ljk3YTQgNCAwIDAgMS0yLjY2IDMuMjgxYy4zNzYtLjgxNC42My0xLjk3NC42ODEtMy4yODFtMS45NzktMUg3Ljk5Yy0uMDUyLTEuMzA3LS4zMDUtMi40NjctLjY4MS0zLjI4MUE0IDQgMCAwIDEgOS45NjkgNS41bS01Ljk2IDBIMi4wMzJhNCA0IDAgMCAxIDIuNjYtMy4yODFjLS4zNzYuODE0LS42MyAxLjk3NC0uNjgxIDMuMjgxIi8+CiAgPC9nPgoKICA8IS0tIFBvcnQgMSAoVG9wLUxlZnQpIC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQsIDQpIHNjYWxlKDAuMDc0KSB0cmFuc2xhdGUoLTEyMywgLTEzNSkiIHN0cm9rZT0iIzAwNzhENCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDxwYXRoIGQ9Ik0xMzkgMTM1aDIzOWM4LjggMCAxNiA3LjIgMTYgMTZ2MjIxYzAgOC44LTcuMiAxNi0xNiAxNkgxMzljLTguOCAwLTE2LTcuMi0xNi0xNlYxNTFjMC04LjggNy4yLTE2IDE2LTE2eiIgc3Ryb2tlLXdpZHRoPSIwLjc1IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMC42IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTk1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIzMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjQ5IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI4NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzAzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogIDwvZz4KCiAgPCEtLSBQb3J0IDIgKEJvdHRvbS1SaWdodCkgLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQsIDI1KSBzY2FsZSgwLjA3NCkgdHJhbnNsYXRlKC0xMjMsIC0xMzUpIiBzdHJva2U9IiMwMDc4RDQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djIyMWMwIDguOC03LjIgMTYtMTYgMTZIMTM5Yy04LjggMC0xNi03LjItMTYtMTZWMTUxYzAtOC44IDcuMi0xNiAxNi0xNnoiIHN0cm9rZS13aWR0aD0iMC43NSIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjAuNiIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE5NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjEzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI0OSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjY3IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMwMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzIxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICA8L2c+Cjwvc3ZnPgo=); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/tunnel_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+TmV0d29yayB0dW5uZWwgKGRpc2FibGVkKTwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgRmx1ZW50IFVJIHN0eWxlIG5ldHdvcmsgdHVubmVsIGljb24gc2hvd2luZyB0d28gZXRoZXJuZXQgcG9ydHMgY29ubmVjdGVkIHZpYSBhIGdsb2JhbCB0dW5uZWwgaW4gYSBkaXNhYmxlZCBzdGF0ZS48L2Rlc2M+CiAgCiAgPCEtLSBDb25uZWN0aW9uIEwtbGluZSBkb3RzIC0tPgogIDxnIGZpbGw9IiMwMDc4RDQiPgogICAgICA8Y2lyY2xlIGN4PSIyNSIgY3k9IjEzLjUiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMzQiIGN5PSIyMi43NSIgcj0iMS41IiAvPgogIDwvZz4KCiAgPCEtLSBDb25uZWN0aW9uIEVhcnRoIEljb24gLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjUuNiwgNS4xKSBzY2FsZSgxLjQpIiBmaWxsPSIjMDA3OEQ0Ij4KICAgICAgPHBhdGggZD0iTTExIDZBNSA1IDAgMSAwIDEgNmE1IDUgMCAwIDAgMTAgMG0tNi45OS41Yy4wNTIgMS4zMDcuMzA1IDIuNDY3LjY4MSAzLjI4MUE0IDQgMCAwIDEgMi4wMzEgNi41em0xIDBoMS45OGMtLjA0NyAxLjA3NS0uMjQgMi4wMDUtLjUwNCAyLjY2NGMtLjE1My4zODMtLjMxLjYyNS0uNDM0Ljc1Nkw2IDkuOTdsLS4wNTItLjA1Yy0uMTIzLS4xMy0uMjgtLjM3My0uNDM0LS43NTZjLS4yNjQtLjY2LS40NTctMS41ODktLjUwMy0yLjY2NG0xLjk4LTFINS4wMWMuMDQ3LTEuMDc1LjI0LTIuMDA1LjUwNC0yLjY2NGMuMTUzLS4zODMuMzEtLjYyNS40MzQtLjc1Nkw2IDIuMDNsLjA1Mi4wNWMuMTIzLjEzLjI4LjM3My40MzQuNzU2Yy4yNjQuNjYuNDU3IDEuNTg5LjUwMyAyLjY2NG0xIDFIOS45N2E0IDQgMCAwIDEtMi42NiAzLjI4MWMuMzc2LS44MTQuNjMtMS45NzQuNjgxLTMuMjgxbTEuOTc5LTFINy45OWMtLjA1Mi0xLjMwNy0uMzA1LTIuNDY3LS42ODEtMy4yODFBNCA0IDAgMCAxIDkuOTY5IDUuNW0tNS45NiAwSDIuMDMyYTQgNCAwIDAgMSAyLjY2LTMuMjgxYy0uMzc2LjgxNC0uNjMgMS45NzQtLjY4MSAzLjI4MSIvPgogIDwvZz4KCiAgPCEtLSBPZmZsaW5lIEJhZGdlIChCb3R0b20tTGVmdCkgLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMywgMjkpIHNjYWxlKDEuNikiIGZpbGw9IiMwMDc4RDQiPgogICAgICA8cGF0aCBkPSJNNi44NTQgMy4xNDZhLjUuNSAwIDAgMSAwIC43MDhMNS43MDcgNWwxLjE0NyAxLjE0NmEuNS41IDAgMSAxLS43MDguNzA4TDUgNS43MDdMMy44NTQgNi44NTRhLjUuNSAwIDEgMS0uNzA4LS43MDhMNC4yOTMgNUwzLjE0NiAzLjg1NGEuNS41IDAgMSAxIC43MDgtLjcwOEw1IDQuMjkzbDEuMTQ2LTEuMTQ3YS41LjUgMCAwIDEgLjcwOCAwTTAgNWE1IDUgMCAxIDEgMTAgMEE1IDUgMCAwIDEgMCA1bTUtNGE0IDQgMCAxIDAgMCA4YTQgNCAwIDAgMCAwLTgiLz4KICA8L2c+CgogIDwhLS0gUG9ydCAxIChUb3AtTGVmdCkgLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNCwgNCkgc2NhbGUoMC4wNzQpIHRyYW5zbGF0ZSgtMTIzLCAtMTM1KSIgc3Ryb2tlPSIjMDA3OEQ0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYyMjFjMCA4LjgtNy4yIDE2LTE2IDE2SDEzOWMtOC44IDAtMTYtNy4yLTE2LTE2VjE1MWMwLTguOCA3LjItMTYgMTYtMTZ6IiBzdHJva2Utd2lkdGg9IjAuNzUiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xNzEgMTg2aDE3M3YxMDFoLTMzdjI3aC0yN3YyM2gtNTV2LTIzaC0yN3YtMjdoLTMxeiIgc3Ryb2tlLXdpZHRoPSIwLjYiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIxMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjMxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI2NyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjg1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMyMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgPC9nPgoKICA8IS0tIFBvcnQgMiAoQm90dG9tLVJpZ2h0KSAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNCwgMjUpIHNjYWxlKDAuMDc0KSB0cmFuc2xhdGUoLTEyMywgLTEzNSkiIHN0cm9rZT0iIzAwNzhENCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDxwYXRoIGQ9Ik0xMzkgMTM1aDIzOWM4LjggMCAxNiA3LjIgMTYgMTZ2MjIxYzAgOC44LTcuMiAxNi0xNiAxNkgxMzljLTguOCAwLTE2LTcuMi0xNi0xNlYxNTFjMC04LjggNy4yLTE2IDE2LTE2eiIgc3Ryb2tlLXdpZHRoPSIwLjc1IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMC42IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTk1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIzMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjQ5IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI4NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzAzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogIDwvZz4KPC9zdmc+Cg==); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/vlan.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMTE1IDgwIDMzNCAzMzQiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5OZXR3b3JrIFZMQU48L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjIj5BIEZsdWVudCBVSSBzdHlsZSBldGhlcm5ldCBwb3J0IGljb24gc2hvd2luZyBhIFZMQU4gdGFnIGJhZGdlLjwvZGVzYz4KICA8ZyBmaWxsPSJub25lIiBzdHJva2U9IiM0MjQyNDIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8IS0tIEN1dG91dCBvdXRlciBib3ggZm9yIGJvdHRvbS1yaWdodCBiYWRnZSAtLT4KICAgIDxwYXRoIGQ9Ik0xMzkgMTM1aDIzOWM4LjggMCAxNiA3LjIgMTYgMTZ2MTIwIE0yNzUgMzg4IEgxMzljLTguOCAwLTE2LTcuMi0xNi0xNlYxNTFjMC04LjggNy4yLTE2IDE2LTE2IiBzdHJva2Utd2lkdGg9IjEwIi8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg2djI2IiBzdHJva2Utd2lkdGg9IjkiLz4KICA8L2c+CiAgPCEtLSBWTEFOIFRhZyBCYWRnZSAoQm90dG9tLVJpZ2h0KSAtLT4KICA8cGF0aCBmaWxsPSIjNDI0MjQyIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNzUgMjc3KSBzY2FsZSg4LjI1KSIgZD0iTTIuNTg3IDcuMzE0YTEuOTkgMS45OSAwIDAgMCAwIDIuODIybDMuMjk2IDMuMjhhMi4wMTMgMi4wMTMgMCAwIDAgMi44MzYgMGw0LjY3LTQuNjQ3YTIgMiAwIDAgMCAuNTg3LTEuMzk3TDE0IDQuMDFBMiAyIDAgMCAwIDExLjk4NyAybC0zLjI1LjAxNGEyIDIgMCAwIDAtMS40MS41ODR6TTExIDZhMSAxIDAgMSAxIDAtMmExIDEgMCAwIDEgMCAyIiAvPgo8L3N2Zz4K); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/vlan_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMTE1IDgwIDMzNCAzMzQiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5OZXR3b3JrIFZMQU4gKGRpc2FibGVkKTwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgRmx1ZW50IFVJIHN0eWxlIGV0aGVybmV0IHBvcnQgaWNvbiBpbiBhIGRpc2FibGVkIHN0YXRlLCBzaG93aW5nIGEgVkxBTiB0YWcgYmFkZ2UgYW5kIGFuIG9mZmxpbmUgYmFkZ2UuPC9kZXNjPgogIDxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQyNDI0MiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDwhLS0gQ3V0b3V0IG91dGVyIGJveCBmb3IgYm90dG9tLWxlZnQgYW5kIGJvdHRvbS1yaWdodCBiYWRnZXMgLS0+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djEyMCBNMjc1IDM4OCBIMjA1IE0xMjMgMzI0VjE1MWMwLTguOCA3LjItMTYgMTYtMTYiIHN0cm9rZS13aWR0aD0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0xNzEgMTg2aDE3M3YxMDFoLTMzdjI3aC0yN3YyM2gtNTV2LTIzaC0yN3YtMjdoLTMxeiIgc3Ryb2tlLXdpZHRoPSIxMCIvPgogICAgPHBhdGggZD0iTTE5NSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTIxMyAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTIzMSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTI0OSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTI2NyAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTI4NSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTMwMyAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTMyMSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogIDwvZz4KICA8IS0tIFZMQU4gVGFnIEJhZGdlIChCb3R0b20tUmlnaHQpIC0tPgogIDxwYXRoIGZpbGw9IiM0MjQyNDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI3NSAyNzcpIHNjYWxlKDguMjUpIiBkPSJNMi41ODcgNy4zMTRhMS45OSAxLjk5IDAgMCAwIDAgMi44MjJsMy4yOTYgMy4yOGEyLjAxMyAyLjAxMyAwIDAgMCAyLjgzNiAwbDQuNjctNC42NDdhMiAyIDAgMCAwIC41ODctMS4zOTdMMTQgNC4wMUEyIDIgMCAwIDAgMTEuOTg3IDJsLTMuMjUuMDE0YTIgMiAwIDAgMC0xLjQxLjU4NHpNMTEgNmExIDEgMCAxIDEgMC0yYTEgMSAwIDAgMSAwIDIiIC8+CiAgPCEtLSBPZmZsaW5lIEJhZGdlIChCb3R0b20tTGVmdCkgLS0+CiAgPHBhdGggZmlsbD0iIzQyNDI0MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIzIDMyNCkgc2NhbGUoOCkiIGQ9Ik02Ljg1NCAzLjE0NmEuNS41IDAgMCAxIDAgLjcwOEw1LjcwNyA1bDEuMTQ3IDEuMTQ2YS41LjUgMCAxIDEtLjcwOC43MDhMNSA1LjcwN0wzLjg1NCA2Ljg1NGEuNS41IDAgMSAxLS43MDgtLjcwOEw0LjI5MyA1TDMuMTQ2IDMuODU0YS41LjUgMCAxIDEgLjcwOC0uNzA4TDUgNC4yOTNsMS4xNDYtMS4xNDdhLjUuNSAwIDAgMSAuNzA4IDBNMCA1YTUgNSAwIDEgMSAxMCAwQTUgNSAwIDAgMSAwIDVtNS00YTQgNCAwIDEgMCAwIDhhNCA0IDAgMCAwIDAtOCIvPgo8L3N2Zz4K); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/alias.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMTE1IDgwIDMzNCAzMzQiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5OZXR3b3JrIGFsaWFzPC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzYyI+QSBGbHVlbnQgVUkgc3R5bGUgZXRoZXJuZXQgcG9ydCBpY29uIHNob3dpbmcgYW4gYWxpYXMgYmFkZ2UuPC9kZXNjPgogIDxnIGZpbGw9Im5vbmUiIHN0cm9rZT0iIzQyNDI0MiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDwhLS0gQ3V0b3V0IG91dGVyIGJveCBmb3IgYm90dG9tLXJpZ2h0IGJhZGdlIC0tPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYxNjAgTTMxNCAzODggSDEzOWMtOC44IDAtMTYtNy4yLTE2LTE2VjE1MWMwLTguOCA3LjItMTYgMTYtMTYiIHN0cm9rZS13aWR0aD0iMTAiLz4KICAgIDxwYXRoIGQ9Ik0xNzEgMTg2aDE3M3YxMDFoLTMzdjI3aC0yN3YyM2gtNTV2LTIzaC0yN3YtMjdoLTMxeiIgc3Ryb2tlLXdpZHRoPSIxMCIvPgogICAgPHBhdGggZD0iTTE5NSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTIxMyAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTIzMSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTI0OSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTI2NyAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTI4NSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTMwMyAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogICAgPHBhdGggZD0iTTMyMSAxODZ2MjYiIHN0cm9rZS13aWR0aD0iOSIvPgogIDwvZz4KICA8IS0tIEFsaWFzIEJhZGdlIChCb3R0b20tUmlnaHQpIC0tPgogIDxwYXRoIGZpbGw9IiM0MjQyNDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwNiAzMDEuNSkgc2NhbGUoNikgcm90YXRlKC00NSA4IDgpIiBkPSJNOS4yMiA0LjI4YS43NS43NSAwIDAgMSAxLjA2LTEuMDZsMy41IDMuNWEuNzUuNzUgMCAwIDEgMCAxLjA2bC0zLjUgMy41YS43NS43NSAwIDEgMS0xLjA2LTEuMDZMMTEuNDQgOEg3Ljc1YTQuMjUgNC4yNSAwIDAgMC00LjI1IDQuMjVhLjc1Ljc1IDAgMCAxLTEuNSAwQTUuNzUgNS43NSAwIDAgMSA3Ljc1IDYuNWgzLjY5eiIgLz4KPC9zdmc+Cg==); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/alias_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMTE1IDgwIDMzNCAzMzQiIHJvbGU9ImltZyIgYXJpYS1sYWJlbGxlZGJ5PSJ0aXRsZSBkZXNjIj4KICA8dGl0bGUgaWQ9InRpdGxlIj5OZXR3b3JrIGFsaWFzIChkaXNhYmxlZCk8L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjIj5BIEZsdWVudCBVSSBzdHlsZSBldGhlcm5ldCBwb3J0IGljb24gaW4gYSBkaXNhYmxlZCBzdGF0ZSwgc2hvd2luZyBhbiBhbGlhcyBiYWRnZSBhbmQgYW4gb2ZmbGluZSBiYWRnZS48L2Rlc2M+CiAgPGcgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjNDI0MjQyIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPCEtLSBDdXRvdXQgb3V0ZXIgYm94IGZvciBib3R0b20tbGVmdCBhbmQgYm90dG9tLXJpZ2h0IGJhZGdlcyAtLT4KICAgIDxwYXRoIGQ9Ik0xMzkgMTM1aDIzOWM4LjggMCAxNiA3LjIgMTYgMTZ2MTYwIE0zMTQgMzg4IEgyMDUgTTEyMyAzMjRWMTUxYzAtOC44IDcuMi0xNiAxNi0xNiIgc3Ryb2tlLXdpZHRoPSIxMCIvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjEwIi8+CiAgICA8cGF0aCBkPSJNMTk1IDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjEzIDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjMxIDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjQ5IDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjY3IDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMjg1IDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMzAzIDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgICA8cGF0aCBkPSJNMzIxIDE4NnYyNiIgc3Ryb2tlLXdpZHRoPSI5Ii8+CiAgPC9nPgogIDwhLS0gQWxpYXMgQmFkZ2UgKEJvdHRvbS1SaWdodCkgLS0+CiAgPHBhdGggZmlsbD0iIzQyNDI0MiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzA2IDMwMS41KSBzY2FsZSg2KSByb3RhdGUoLTQ1IDggOCkiIGQ9Ik05LjIyIDQuMjhhLjc1Ljc1IDAgMCAxIDEuMDYtMS4wNmwzLjUgMy41YS43NS43NSAwIDAgMSAwIDEuMDZsLTMuNSAzLjVhLjc1Ljc1IDAgMSAxLTEuMDYtMS4wNkwxMS40NCA4SDcuNzVhNC4yNSA0LjI1IDAgMCAwLTQuMjUgNC4yNWEuNzUuNzUgMCAwIDEtMS41IDBBNS43NSA1Ljc1IDAgMCAxIDcuNzUgNi41aDMuNjl6IiAvPgogIDwhLS0gT2ZmbGluZSBCYWRnZSAoQm90dG9tLUxlZnQpIC0tPgogIDxwYXRoIGZpbGw9IiM0MjQyNDIiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMyAzMjQpIHNjYWxlKDgpIiBkPSJNNi44NTQgMy4xNDZhLjUuNSAwIDAgMSAwIC43MDhMNS43MDcgNWwxLjE0NyAxLjE0NmEuNS41IDAgMSAxLS43MDguNzA4TDUgNS43MDdMMy44NTQgNi44NTRhLjUuNSAwIDEgMS0uNzA4LS43MDhMNC4yOTMgNUwzLjE0NiAzLjg1NGEuNS41IDAgMSAxIC43MDgtLjcwOEw1IDQuMjkzbDEuMTQ2LTEuMTQ3YS41LjUgMCAwIDEgLjcwOCAwTTAgNWE1IDUgMCAxIDEgMTAgMEE1IDUgMCAwIDEgMCA1bTUtNGE0IDQgMCAxIDAgMCA4YTQgNCAwIDAgMCAwLTgiLz4KPC9zdmc+Cg==); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/switch.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+TmV0d29yayBzd2l0Y2g8L3RpdGxlPgogIDxkZXNjIGlkPSJkZXNjIj5BIEZsdWVudCBVSSBzdHlsZSBuZXR3b3JrIHN3aXRjaCBpY29uIHNob3dpbmcgdGhyZWUgZXRoZXJuZXQgcG9ydHMgY29ubmVjdGVkIHZpYSBhIGNlbnRyYWwgYnVzLjwvZGVzYz4KICAKICA8IS0tIENvbm5lY3Rpb24gYnVzIGRvdHMgLS0+CiAgPGcgZmlsbD0iIzAwNzhENCI+CiAgICAgIDxjaXJjbGUgY3g9IjEzLjUiIGN5PSIyNCIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIxNy4xNiIgY3k9IjI0IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjIwLjgzIiBjeT0iMjQiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMjQuNSIgY3k9IjI0IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjI4LjE2IiBjeT0iMjQiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMzEuODMiIGN5PSIyNCIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIzNS41IiBjeT0iMjQiIHI9IjEuNSIgLz4KICA8L2c+CgogIDwhLS0gUG9ydCAxIChUb3AtTGVmdCkgLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNCwgMikgc2NhbGUoMC4wNzQpIHRyYW5zbGF0ZSgtMTIzLCAtMTM1KSIgc3Ryb2tlPSIjMDA3OEQ0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYyMjFjMCA4LjgtNy4yIDE2LTE2IDE2SDEzOWMtOC44IDAtMTYtNy4yLTE2LTE2VjE1MWMwLTguOCA3LjItMTYgMTYtMTZ6IiBzdHJva2Utd2lkdGg9IjAuNzUiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xNzEgMTg2aDE3M3YxMDFoLTMzdjI3aC0yN3YyM2gtNTV2LTIzaC0yN3YtMjdoLTMxeiIgc3Ryb2tlLXdpZHRoPSIwLjYiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIxMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjMxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI2NyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjg1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMyMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgPC9nPgoKICA8IS0tIFBvcnQgMiAoVG9wLVJpZ2h0KSAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNiwgMikgc2NhbGUoMC4wNzQpIHRyYW5zbGF0ZSgtMTIzLCAtMTM1KSIgc3Ryb2tlPSIjMDA3OEQ0IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPgogICAgPHBhdGggZD0iTTEzOSAxMzVoMjM5YzguOCAwIDE2IDcuMiAxNiAxNnYyMjFjMCA4LjgtNy4yIDE2LTE2IDE2SDEzOWMtOC44IDAtMTYtNy4yLTE2LTE2VjE1MWMwLTguOCA3LjItMTYgMTYtMTZ6IiBzdHJva2Utd2lkdGg9IjAuNzUiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xNzEgMTg2aDE3M3YxMDFoLTMzdjI3aC0yN3YyM2gtNTV2LTIzaC0yN3YtMjdoLTMxeiIgc3Ryb2tlLXdpZHRoPSIwLjYiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0xOTUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIxMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjMxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNDkgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI2NyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjg1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMDMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMyMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgPC9nPgoKICA8IS0tIFBvcnQgMyAoQm90dG9tLUNlbnRlcikgLS0+CiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUsIDI3KSBzY2FsZSgwLjA3NCkgdHJhbnNsYXRlKC0xMjMsIC0xMzUpIiBzdHJva2U9IiMwMDc4RDQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djIyMWMwIDguOC03LjIgMTYtMTYgMTZIMTM5Yy04LjggMC0xNi03LjItMTYtMTZWMTUxYzAtOC44IDcuMi0xNiAxNi0xNnoiIHN0cm9rZS13aWR0aD0iMC43NSIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjAuNiIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE5NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjEzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI0OSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjY3IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMwMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzIxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICA8L2c+Cjwvc3ZnPgo=); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/switch_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --icon-data: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDQ4IDQ4IiBmaWxsPSJub25lIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyI+CiAgPHRpdGxlIGlkPSJ0aXRsZSI+TmV0d29yayBzd2l0Y2ggKGRpc2FibGVkKTwvdGl0bGU+CiAgPGRlc2MgaWQ9ImRlc2MiPkEgRmx1ZW50IFVJIHN0eWxlIG5ldHdvcmsgc3dpdGNoIGljb24gc2hvd2luZyB0aHJlZSBldGhlcm5ldCBwb3J0cyBjb25uZWN0ZWQgdmlhIGEgY2VudHJhbCBidXMgaW4gYSBkaXNhYmxlZCBzdGF0ZS48L2Rlc2M+CiAgCiAgPCEtLSBDb25uZWN0aW9uIGJ1cyBkb3RzIC0tPgogIDxnIGZpbGw9IiMwMDc4RDQiPgogICAgICA8Y2lyY2xlIGN4PSIxMy41IiBjeT0iMjQiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMTcuMTYiIGN5PSIyNCIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIyMC44MyIgY3k9IjI0IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjI0LjUiIGN5PSIyNCIgcj0iMS41IiAvPgogICAgICA8Y2lyY2xlIGN4PSIyOC4xNiIgY3k9IjI0IiByPSIxLjUiIC8+CiAgICAgIDxjaXJjbGUgY3g9IjMxLjgzIiBjeT0iMjQiIHI9IjEuNSIgLz4KICAgICAgPGNpcmNsZSBjeD0iMzUuNSIgY3k9IjI0IiByPSIxLjUiIC8+CiAgPC9nPgoKICA8IS0tIE9mZmxpbmUgQmFkZ2UgKEJvdHRvbS1MZWZ0KSAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzLCAyOSkgc2NhbGUoMS42KSIgZmlsbD0iIzAwNzhENCI+CiAgICAgIDxwYXRoIGQ9Ik02Ljg1NCAzLjE0NmEuNS41IDAgMCAxIDAgLjcwOEw1LjcwNyA1bDEuMTQ3IDEuMTQ2YS41LjUgMCAxIDEtLjcwOC43MDhMNSA1LjcwN0wzLjg1NCA2Ljg1NGEuNS41IDAgMSAxLS43MDgtLjcwOEw0LjI5MyA1TDMuMTQ2IDMuODU0YS41LjUgMCAxIDEgLjcwOC0uNzA4TDUgNC4yOTNsMS4xNDYtMS4xNDdhLjUuNSAwIDAgMSAuNzA4IDBNMCA1YTUgNSAwIDEgMSAxMCAwQTUgNSAwIDAgMSAwIDVtNS00YTQgNCAwIDEgMCAwIDhhNCA0IDAgMCAwIDAtOCIvPgogIDwvZz4KCiAgPCEtLSBQb3J0IDEgKFRvcC1MZWZ0KSAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0LCAyKSBzY2FsZSgwLjA3NCkgdHJhbnNsYXRlKC0xMjMsIC0xMzUpIiBzdHJva2U9IiMwMDc4RDQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djIyMWMwIDguOC03LjIgMTYtMTYgMTZIMTM5Yy04LjggMC0xNi03LjItMTYtMTZWMTUxYzAtOC44IDcuMi0xNiAxNi0xNnoiIHN0cm9rZS13aWR0aD0iMC43NSIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjAuNiIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE5NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjEzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI0OSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjY3IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMwMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzIxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICA8L2c+CgogIDwhLS0gUG9ydCAyIChUb3AtUmlnaHQpIC0tPgogIDxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2LCAyKSBzY2FsZSgwLjA3NCkgdHJhbnNsYXRlKC0xMjMsIC0xMzUpIiBzdHJva2U9IiMwMDc4RDQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+CiAgICA8cGF0aCBkPSJNMTM5IDEzNWgyMzljOC44IDAgMTYgNy4yIDE2IDE2djIyMWMwIDguOC03LjIgMTYtMTYgMTZIMTM5Yy04LjggMC0xNi03LjItMTYtMTZWMTUxYzAtOC44IDcuMi0xNiAxNi0xNnoiIHN0cm9rZS13aWR0aD0iMC43NSIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE3MSAxODZoMTczdjEwMWgtMzN2MjdoLTI3djIzaC01NXYtMjNoLTI3di0yN2gtMzF6IiBzdHJva2Utd2lkdGg9IjAuNiIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTE5NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjEzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMzEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI0OSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjY3IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yODUgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTMwMyAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzIxIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICA8L2c+CgogIDwhLS0gUG9ydCAzIChCb3R0b20tQ2VudGVyKSAtLT4KICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxNSwgMjcpIHNjYWxlKDAuMDc0KSB0cmFuc2xhdGUoLTEyMywgLTEzNSkiIHN0cm9rZT0iIzAwNzhENCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj4KICAgIDxwYXRoIGQ9Ik0xMzkgMTM1aDIzOWM4LjggMCAxNiA3LjIgMTYgMTZ2MjIxYzAgOC44LTcuMiAxNi0xNiAxNkgxMzljLTguOCAwLTE2LTcuMi0xNi0xNlYxNTFjMC04LjggNy4yLTE2IDE2LTE2eiIgc3Ryb2tlLXdpZHRoPSIwLjc1IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTcxIDE4NmgxNzN2MTAxaC0zM3YyN2gtMjd2MjNoLTU1di0yM2gtMjd2LTI3aC0zMXoiIHN0cm9rZS13aWR0aD0iMC42IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMTk1IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yMTMgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTIzMSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMjQ5IDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0yNjcgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogICAgPHBhdGggZD0iTTI4NSAxODh2MjYiIHN0cm9rZS13aWR0aD0iMC40IiB2ZWN0b3ItZWZmZWN0PSJub24tc2NhbGluZy1zdHJva2UiIC8+CiAgICA8cGF0aCBkPSJNMzAzIDE4OHYyNiIgc3Ryb2tlLXdpZHRoPSIwLjQiIHZlY3Rvci1lZmZlY3Q9Im5vbi1zY2FsaW5nLXN0cm9rZSIgLz4KICAgIDxwYXRoIGQ9Ik0zMjEgMTg4djI2IiBzdHJva2Utd2lkdGg9IjAuNCIgdmVjdG9yLWVmZmVjdD0ibm9uLXNjYWxpbmctc3Ryb2tlIiAvPgogIDwvZz4KPC9zdmc+Cg==); + width: 24px; + height: 24px; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/signal-000-000.svg"] { + vertical-align: middle; + width: 24px; + height: 24px; + display: inline-block; + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(0,0,0,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23616161%27%20d=%27M4.75%2012a.75.75%200%200%201%20.743.64l.008.11v2.503a.75.75%200%200%201-1.493.11L4%2015.254V12.75a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +[data-theme="dark"] img[src="/luci-static/resources/icons/signal-000-000.svg"] { + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(255,255,255,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23adadad%27%20d=%27M4.75%2012a.75.75%200%200%201%20.743.64l.008.11v2.503a.75.75%200%200%201-1.493.11L4%2015.254V12.75a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +img[src="/luci-static/resources/icons/signal-000-025.svg"] { + vertical-align: middle; + width: 24px; + height: 24px; + display: inline-block; + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(0,0,0,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23616161%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +[data-theme="dark"] img[src="/luci-static/resources/icons/signal-000-025.svg"] { + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(255,255,255,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23adadad%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +img[src="/luci-static/resources/icons/signal-025-050.svg"] { + vertical-align: middle; + width: 24px; + height: 24px; + display: inline-block; + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(0,0,0,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23616161%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.496a.75.75%200%200%201-1.492.11L4%2015.246V12.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.496a.75.75%200%200%201-1.492.11L7%2015.246V10.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +[data-theme="dark"] img[src="/luci-static/resources/icons/signal-025-050.svg"] { + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(255,255,255,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23adadad%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.496a.75.75%200%200%201-1.492.11L4%2015.246V12.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.496a.75.75%200%200%201-1.492.11L7%2015.246V10.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +img[src="/luci-static/resources/icons/signal-050-075.svg"] { + vertical-align: middle; + width: 24px; + height: 24px; + display: inline-block; + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(0,0,0,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23616161%27%20d=%27M4.75%2012a.75.75%200%200%201%20.741.639l.009.11v2.496a.75.75%200%200%201-1.492.11L4%2015.246V12.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.741.639l.009.11v4.496a.75.75%200%200%201-1.492.11L7%2015.246V10.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.741.639l.008.11v6.501a.75.75%200%200%201-1.49.111l-.009-.11V8.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.741.639l.008.11v8.501a.75.75%200%200%201-1.49.111l-.009-.11V6.75a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +[data-theme="dark"] img[src="/luci-static/resources/icons/signal-050-075.svg"] { + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(255,255,255,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23adadad%27%20d=%27M4.75%2012a.75.75%200%200%201%20.741.639l.009.11v2.496a.75.75%200%200%201-1.492.11L4%2015.246V12.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.741.639l.009.11v4.496a.75.75%200%200%201-1.492.11L7%2015.246V10.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.741.639l.008.11v6.501a.75.75%200%200%201-1.49.111l-.009-.11V8.75a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.741.639l.008.11v8.501a.75.75%200%200%201-1.49.111l-.009-.11V6.75a.75.75%200%200%201%20.75-.75%27/%3E%3C/svg%3E") !important; +} + +img[src="/luci-static/resources/icons/signal-075-100.svg"] { + vertical-align: middle; + width: 24px; + height: 24px; + display: inline-block; + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(0,0,0,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23616161%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3C/svg%3E") !important; +} + +[data-theme="dark"] img[src="/luci-static/resources/icons/signal-075-100.svg"] { + content: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org/2000/svg%27%20width=%2720%27%20height=%2720%27%20viewBox=%270%200%2020%2020%27%20fill=%27none%27%3E%3Cpath%20fill=%27rgba(255,255,255,0.18)%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3Cpath%20fill=%27%23adadad%27%20d=%27M4.75%2012a.75.75%200%200%201%20.742.64l.008.11v2.5a.75.75%200%200%201-1.492.11L4%2015.25v-2.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v4.5a.75.75%200%200%201-1.492.11L7%2015.25v-4.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v6.5a.75.75%200%200%201-1.492.11L10%2015.25v-6.5a.75.75%200%200%201%20.75-.75m3-2a.75.75%200%200%201%20.742.64l.008.11v8.499a.75.75%200%200%201-1.492.11L13%2015.25v-8.5a.75.75%200%200%201%20.75-.75m3-2a.74.74%200%200%201%20.74.626l.01.11l-.014%2010.515a.743.743%200%200%201-1.477.124l-.009-.11l.014-10.515A.743.743%200%200%201%2016.75%204%27/%3E%3C/svg%3E") !important; +} + +img[src="/luci-static/resources/icons/signal-none.svg"] { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-cellular-warning-20-filled: url("data:image/svg+xml,"); + width: 24px; + height: 24px; + -webkit-mask-image: var(--fluent-mask-fluent-cellular-warning-20-filled); + mask-image: var(--fluent-mask-fluent-cellular-warning-20-filled); + -webkit-user-select: none; + user-select: none; + content: url("data:image/svg+xml;utf8,"); + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/wifi.svg"] { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-wifi-1-20-filled: url("data:image/svg+xml,"); + width: 24px; + height: 24px; + -webkit-mask-image: var(--fluent-mask-fluent-wifi-1-20-filled); + mask-image: var(--fluent-mask-fluent-wifi-1-20-filled); + -webkit-user-select: none; + user-select: none; + content: url("data:image/svg+xml;utf8,"); + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +img[src="/luci-static/resources/icons/wifi_disabled.svg"] { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-wifi-off-20-filled: url("data:image/svg+xml,"); + width: 24px; + height: 24px; + -webkit-mask-image: var(--fluent-mask-fluent-wifi-off-20-filled); + mask-image: var(--fluent-mask-fluent-wifi-off-20-filled); + -webkit-user-select: none; + user-select: none; + content: url("data:image/svg+xml;utf8,"); + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent { + --text-color-high: var(--fluent-text); + --text-color-medium: var(--fluent-text-secondary); + --text-color-low: var(--fluent-text-secondary); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .dashboard-bg { + background: var(--fluent-card-bg); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-card-radius); + box-shadow: var(--fluent-card-shadow); + padding: var(--fluent-card-padding); + flex: 1 1 calc(50% - var(--fluent-space-lg)); + box-sizing: border-box; + min-width: 320px; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .router-status-wifi .settings-info { + gap: var(--fluent-space-md); + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + display: grid; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .radio-info { + background: var(--fluent-bg); + border: 1px solid var(--fluent-table-border); + border-radius: var(--fluent-radius-md); + padding: var(--fluent-space-sm) var(--fluent-space-md); + font-size: var(--fluent-font-size-sm); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .radio-info p { + line-height: var(--fluent-line-height-normal); + margin: 3px 0; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .radio-info p > span:first-child { + color: var(--fluent-text-secondary); + min-width: 100px; + display: inline-block; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .label { + border-radius: var(--fluent-radius-sm); + font-size: var(--fluent-font-size-xs); + font-weight: var(--fluent-font-weight-semibold); + text-transform: uppercase; + letter-spacing: .03em; + padding: 1px 8px; + line-height: 1.6; + display: inline-block; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .label-success { + background: var(--fluent-success-light); + color: var(--fluent-success); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .label-danger { + background: var(--fluent-error-light); + color: var(--fluent-error); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .label-warning { + background: var(--fluent-warning-light); + color: #866b00; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist { + border-collapse: collapse; + width: 100%; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist .thead { + background: var(--fluent-table-header-bg); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist .th { + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text-secondary); + border-bottom: 2px solid var(--fluent-table-border); + white-space: nowrap; + text-align: left; + padding: 8px 12px; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist .td { + font-size: var(--fluent-font-size-sm); + color: var(--fluent-text); + border-bottom: 1px solid var(--fluent-table-border); + vertical-align: middle; + padding: 8px 12px; + position: static; + top: auto; + overflow: visible; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist .td:nth-child(3) { + min-width: 140px; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist .tr:hover .td { + background: var(--fluent-table-row-hover); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .devices-list .table.assoclist .tfoot .td { + font-weight: var(--fluent-font-weight-semibold); + border-top: 2px solid var(--fluent-table-border); + border-bottom: none; +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .wireless-table-transfer br + span { + font-size: var(--fluent-font-size-xs); +} + +body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .cbi-progressbar { + min-width: 120px; + background: none !important; +} + +@media (max-width: 767px) { + body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .dashboard-bg { + flex: 100%; + min-width: 0; + } + + body[data-page="admin-dashboard"] > .main > .main-right > #maincontent .settings-info { + grid-template-columns: 1fr !important; + } +} + +body[data-page="admin-status-nftables-iptables"] > .main > .main-right > #maincontent #view > div.right { + margin-top: 4px; + margin-bottom: 0 !important; +} + +body[data-page="admin-status-channel_analysis"] > .main > .main-right > #maincontent #view > div > div.cbi-title-section { + display: flex; +} + +body[data-page="admin-status-channel_analysis"] > .main > .main-right > #maincontent #view > div > div.cbi-title-section > div { + margin-left: auto; +} + +body[data-page="admin-status-channel_analysis"] > .main > .main-right > #maincontent #channel_graph { + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-card-radius); + background: var(--fluent-card-bg) !important; +} + +body[data-page="admin-status-channel_analysis"] > .main > .main-right > #maincontent #channel_graph svg line[style*="stroke:black"] { + stroke: var(--fluent-text-secondary) !important; +} + +body[data-page="admin-status-channel_analysis"] > .main > .main-right > #maincontent div[data-tab]:not([data-tab-active="true"]):not([tab-active="true"]) { + visibility: hidden !important; + width: 100% !important; + height: 0 !important; + display: block !important; + position: absolute !important; + overflow: hidden !important; +} + +body[data-page^="admin-status-realtime"] > .main > .main-right > #maincontent .cbi-section div[style*="background:#fff"] { + background: var(--fluent-card-bg) !important; + border-radius: var(--fluent-card-radius) !important; + border: none !important; +} + +body[data-page^="admin-status-realtime"] > .main > .main-right > #maincontent .cbi-section svg line[style*="stroke:black"] { + stroke: var(--fluent-text-secondary) !important; +} + +body[data-page^="admin-status-overview"] > .main > .main-right > #maincontent .ifacebox > .ifacebox-body > br { + display: none; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager { + justify-content: center; + gap: var(--fluent-space-xs); + box-sizing: border-box; + width: 100%; + padding: 0; + align-items: center !important; + display: flex !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button { + border-radius: var(--fluent-radius-md); + min-width: 32px; + height: 32px; + font-weight: var(--fluent-font-weight-semibold); + color: var(--fluent-text); + background: var(--fluent-btn-secondary-bg); + border: 1px solid var(--fluent-btn-secondary-border); + cursor: pointer; + -webkit-user-select: none; + user-select: none; + box-sizing: border-box; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard),transform var(--fluent-duration-fast) var(--fluent-easing-standard); + justify-content: center; + align-items: center; + margin: 0; + display: inline-flex; + flex: none !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn.prev, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn.next, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button.prev, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button.next, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button.prev, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button.next { + width: 32px; + padding: 0; + font-size: 0 !important; + line-height: 0 !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn.prev:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn.next:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button.prev:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button.next:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button.prev:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button.next:before { + content: ""; + width: 16px; + height: 16px; + display: inline-block; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn.prev:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button.prev:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button.prev:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-chevron-left-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-chevron-left-20-regular); + mask-image: var(--fluent-mask-chevron-left-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .btn.next:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .cbi-button.next:before, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > button.next:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-chevron-right-20-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-chevron-right-20-regular); + mask-image: var(--fluent-mask-chevron-right-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > .text, body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .pager > span { + padding: 0 var(--fluent-space-md); + font-size: var(--fluent-font-size-sm); + color: var(--fluent-text-secondary); + font-weight: var(--fluent-font-weight-normal); + -webkit-user-select: none; + user-select: none; + white-space: nowrap; + line-height: 32px; + flex: none !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .controls > .pager { + flex: 100% !important; + max-width: 100% !important; +} + +body[data-page="admin-system-package-manager"] > .main > .main-right > #maincontent .controls > .pager > * { + flex: none !important; + align-self: center !important; +} + +body[data-page="admin-system-package-manager"] > #modal_overlay div.modal > .modal-content-wrap > ul:first-child { + max-height: calc(85dvh - 300px); + overflow-y: auto; +} + +body[data-page="admin-system-package-manager"] > #modal_overlay div.modal > .modal-content-wrap > ul.errors { + margin-left: unset !important; + padding-inline-start: unset !important; +} + +body[data-page="admin-system-package-manager"] > #modal_overlay div.modal > div:has(hr:first-child) > hr:first-child { + display: none; +} + +body[data-page="admin-system-package-manager"] > #modal_overlay div.modal > div:has(hr:first-child) > p:nth-child(2) { + margin-top: var(--fluent-space-lg) !important; + margin-bottom: unset !important; +} + +body[data-page^="admin-status-logs-syslog"] textarea#syslog, body[data-page^="admin-status-logs-dmesg"] textarea#syslog { + display: none; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer { + background-color: var(--fluent-card-bg); + border: 1px solid var(--fluent-card-border); + border-radius: var(--fluent-card-radius); + box-shadow: var(--fluent-card-shadow); + margin-bottom: var(--fluent-space-lg); + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard),box-shadow var(--fluent-duration-fast) var(--fluent-easing-standard); + flex-direction: column; + display: flex; + overflow: hidden; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__header, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__header { + justify-content: space-between; + align-items: center; + gap: var(--fluent-space-sm); + padding: var(--fluent-space-sm) var(--fluent-space-md); + background-color: var(--fluent-bg-hover); + border-bottom: 1px solid var(--fluent-card-border); + flex-wrap: wrap; + display: flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__stats, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__stats { + align-items: center; + gap: var(--fluent-space-xs); + flex-wrap: wrap; + display: flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge { + border-radius: var(--fluent-radius-circular); + font-size: var(--fluent-font-size-sm); + font-weight: var(--fluent-font-weight-semibold); + align-items: center; + gap: 4px; + padding: 3px 10px; + line-height: 1; + display: inline-flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge--lines, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge--lines { + background-color: color-mix(in srgb, var(--fluent-text-secondary) 12%, transparent); + color: var(--fluent-text-secondary); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge--errors, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge--errors { + background-color: var(--fluent-error-light); + color: var(--fluent-error); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge--warnings, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge--warnings { + background-color: var(--fluent-warning-light); + color: var(--fluent-warning); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge--disconnects, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge--disconnects { + background-color: var(--fluent-purple-light); + color: var(--fluent-purple); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge-icon, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge-icon { + width: 14px; + height: 14px; + display: inline-block; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge-icon--lines, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge-icon--lines { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-document-16-regular: url("data:image/svg+xml,"); + width: 14px; + height: 14px; + -webkit-mask-image: var(--fluent-mask-fluent-document-16-regular); + mask-image: var(--fluent-mask-fluent-document-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge-icon--errors, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge-icon--errors { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-error-circle-16-regular: url("data:image/svg+xml,"); + width: 14px; + height: 14px; + -webkit-mask-image: var(--fluent-mask-fluent-error-circle-16-regular); + mask-image: var(--fluent-mask-fluent-error-circle-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge-icon--warnings, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge-icon--warnings { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-warning-16-regular: url("data:image/svg+xml,"); + width: 14px; + height: 14px; + -webkit-mask-image: var(--fluent-mask-fluent-warning-16-regular); + mask-image: var(--fluent-mask-fluent-warning-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__badge-icon--disconnects, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__badge-icon--disconnects { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-plug-disconnected-16-regular: url("data:image/svg+xml,"); + width: 14px; + height: 14px; + -webkit-mask-image: var(--fluent-mask-fluent-plug-disconnected-16-regular); + mask-image: var(--fluent-mask-fluent-plug-disconnected-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__toolbar, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__toolbar { + align-items: center; + gap: 2px; + display: flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__tool-btn, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__tool-btn { + border-radius: var(--fluent-radius-md); + width: 32px; + height: 32px; + color: var(--fluent-text-secondary); + cursor: pointer; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard); + background: none; + border: none; + outline: none; + justify-content: center; + align-items: center; + padding: 0; + display: inline-flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__tool-btn:hover, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__tool-btn:hover { + background-color: var(--fluent-bg-hover); + color: var(--fluent-text); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__tool-btn:focus-visible, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__tool-btn:focus-visible { + outline: 2px solid var(--fluent-primary); + outline-offset: -2px; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__tool-btn.active, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__tool-btn.active { + background-color: var(--fluent-bg-active); + color: var(--fluent-primary); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__tool-btn--feedback, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__tool-btn--feedback { + background-color: var(--fluent-success-light); + color: var(--fluent-success); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon { + width: 16px; + height: 16px; + display: inline-block; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--wrap, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--wrap { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-text-wrap-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-text-wrap-16-regular); + mask-image: var(--fluent-mask-fluent-text-wrap-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--autoscroll, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--autoscroll { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-clock-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-clock-16-regular); + mask-image: var(--fluent-mask-fluent-clock-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--copy, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--copy { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-copy-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-copy-16-regular); + mask-image: var(--fluent-mask-fluent-copy-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--download, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--download { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-arrow-download-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-arrow-download-16-regular); + mask-image: var(--fluent-mask-fluent-arrow-download-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--scroll-top, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--scroll-top { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-arrow-up-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-arrow-up-16-regular); + mask-image: var(--fluent-mask-fluent-arrow-up-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--scroll-bottom, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--scroll-bottom { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-arrow-down-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-arrow-down-16-regular); + mask-image: var(--fluent-mask-fluent-arrow-down-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--fullscreen, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--fullscreen { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-full-screen-maximize-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-full-screen-maximize-16-regular); + mask-image: var(--fluent-mask-fluent-full-screen-maximize-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__icon--fullscreen-exit, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__icon--fullscreen-exit { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-fluent-full-screen-minimize-16-regular: url("data:image/svg+xml,"); + width: 16px; + height: 16px; + -webkit-mask-image: var(--fluent-mask-fluent-full-screen-minimize-16-regular); + mask-image: var(--fluent-mask-fluent-full-screen-minimize-16-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__body, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__body { + max-height: 600px; + font-family: var(--fluent-font-mono, Consolas, Monaco, "Courier New", monospace); + font-size: var(--fluent-font-size-sm); + background-color: var(--fluent-bg-card); + color: var(--fluent-text); + padding: var(--fluent-space-xs) 0; + scroll-behavior: smooth; + line-height: 1.6; + overflow: auto; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__content, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__content { + box-sizing: border-box; + flex-direction: column; + width: 100%; + min-width: max-content; + display: flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line { + padding: 1px var(--fluent-space-sm); + white-space: pre; + box-sizing: border-box; + align-items: baseline; + gap: 6px; + min-width: 0; + display: flex; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line:hover, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line:hover { + background-color: var(--fluent-bg-hover); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--err, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--err { + background-color: color-mix(in srgb, var(--fluent-error) 8%, transparent); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--err .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--err .fluent-log-viewer__line-number { + border-left-color: var(--fluent-error); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--warn, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--warn { + background-color: color-mix(in srgb, var(--fluent-warning) 8%, transparent); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--warn .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--warn .fluent-log-viewer__line-number { + border-left-color: var(--fluent-warning); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--info .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--notice .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--info .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--notice .fluent-log-viewer__line-number { + border-left-color: var(--fluent-info); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line--disconnect .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line--disconnect .fluent-log-viewer__line-number { + border-left-color: var(--fluent-purple); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line-number, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line-number { + text-align: right; + -webkit-user-select: none; + user-select: none; + min-width: 34px; + color: var(--fluent-text-disabled); + border-left: 2px solid #0000; + flex-shrink: 0; + padding: 0 4px 0 6px; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__line-time, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__line-time { + color: var(--fluent-text-secondary); + font-size: var(--fluent-font-size-xs); + flex-shrink: 0; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level { + border-radius: var(--fluent-radius-sm); + font-size: var(--fluent-font-size-xs); + font-weight: var(--fluent-font-weight-semibold); + background-color: var(--fluent-bg-hover); + color: var(--fluent-text); + flex-shrink: 0; + padding: 1px 6px; + display: inline-block; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--err, body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--crit, body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--alert, body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--emerg, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--err, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--crit, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--alert, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--emerg { + background-color: var(--fluent-error-light); + color: var(--fluent-error); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--warn, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--warn { + background-color: var(--fluent-warning-light); + color: var(--fluent-warning); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--info, body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__level--notice, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--info, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__level--notice { + background-color: var(--fluent-info-light); + color: var(--fluent-info); +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__tag, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__tag { + color: var(--fluent-primary); + font-weight: var(--fluent-font-weight-semibold); + flex-shrink: 0; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer__text, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer__text { + color: var(--fluent-text); + flex: 1; + min-width: 0; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer--wrap .fluent-log-viewer__body, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer--wrap .fluent-log-viewer__body { + overflow-x: hidden !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer--wrap .fluent-log-viewer__content, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer--wrap .fluent-log-viewer__content { + width: 100% !important; + min-width: 0 !important; + max-width: 100% !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer--wrap .fluent-log-viewer__line, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer--wrap .fluent-log-viewer__line { + white-space: pre-wrap !important; + word-break: break-all !important; + overflow-wrap: anywhere !important; + width: 100% !important; + max-width: 100% !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer--wrap .fluent-log-viewer__text, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer--wrap .fluent-log-viewer__text { + white-space: pre-wrap !important; + word-break: break-all !important; + overflow-wrap: anywhere !important; + flex: 1 !important; + min-width: 0 !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer--fullscreen, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer--fullscreen { + z-index: 999999 !important; + background-color: var(--fluent-card-bg) !important; + width: 100vw !important; + height: 100dvh !important; + box-shadow: none !important; + border: none !important; + border-radius: 0 !important; + margin: 0 !important; + position: fixed !important; + inset: 0 !important; +} + +body[data-page^="admin-status-logs-syslog"] .fluent-log-viewer--fullscreen .fluent-log-viewer__body, body[data-page^="admin-status-logs-dmesg"] .fluent-log-viewer--fullscreen .fluent-log-viewer__body { + flex: 1 !important; + max-height: none !important; +} + +body.fluent-log-fullscreen { + overflow: hidden !important; +} + +body[data-page="admin-network-wireless"] > #modal_overlay > .modal { + max-height: 90% !important; +} + +body[data-page^="admin-services-appfilter"] > .main > .main-right > #maincontent .slider { + background-color: var(--background-color-medium); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent { + --pb-input-bg: var(--fluent-input-bg); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-preview-box { + background: var(--fluent-card-bg); + color: var(--fluent-text-secondary); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-tab:hover:not(.is-active) { + background: var(--fluent-bg-hover); + border-color: var(--fluent-border-hover); + color: var(--fluent-text); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-mode-card { + background: var(--fluent-card-bg); + border-color: var(--fluent-card-border); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-inline-field { + background: var(--fluent-input-bg); + border-color: var(--fluent-input-border); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-card-title { + color: var(--fluent-text); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-card-subtitle, :root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-inline-title, :root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-inline-description, :root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-tab { + color: var(--fluent-text-secondary); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-tab-bar { + background: var(--fluent-bg-hover); + border-color: var(--fluent-border); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-tab.is-active { + background: var(--fluent-card-bg); + color: var(--fluent-text); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-choice-button:not(.is-on) { + background: var(--fluent-btn-secondary-bg); + border-color: var(--fluent-btn-secondary-border); + color: var(--fluent-text); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-sel-list { + background: var(--fluent-card-bg); + border-color: var(--fluent-card-border); +} + +:root[data-theme="dark"] body[data-page="admin-services-pushbot"] > .main > .main-right > #maincontent .pushbot-sel-item { + color: var(--fluent-text); +} + +:root, [data-theme="light"] { + --lightningcss-light: initial; + --lightningcss-dark: ; + color-scheme: light; + --fluent-primary: #0078d4; + --fluent-primary-hover: var(--fluent-brand-70); + --fluent-primary-active: var(--fluent-brand-60); + --fluent-primary-light: var(--fluent-brand-140); + --fluent-primary-lighter: var(--fluent-brand-150); + --fluent-primary-lightest: var(--fluent-brand-160); + --fluent-selection-bg: var(--fluent-primary-light); + --fluent-selection-text: var(--fluent-text); + --fluent-white: #fff; + --fluent-black: #000; + --fluent-bg: #fff; + --fluent-bg-hover: #f5f5f5; + --fluent-bg-active: #ebebeb; + --fluent-bg-card: #fff; + --fluent-bg-page: #fafafa; + --fluent-text: #242424; + --fluent-text-secondary: #616161; + --fluent-text-disabled: #a0a0a0; + --fluent-text-on-primary: #fff; + --fluent-text-link: var(--fluent-primary); + --fluent-text-link-hover: var(--fluent-primary-hover); + --fluent-border: #d1d1d1; + --fluent-border-hover: #a0a0a0; + --fluent-border-active: var(--fluent-primary); + --fluent-success: #107c10; + --fluent-success-light: #f1faf1; + --fluent-warning: #ffb900; + --fluent-warning-light: #fff4ce; + --fluent-error: #d13438; + --fluent-error-hover: #b42b2f; + --fluent-error-active: #9a2226; + --fluent-error-light: #fde7e9; + --fluent-info: var(--fluent-primary); + --fluent-info-light: var(--fluent-primary-lightest); + --fluent-purple: #7e22ce; + --fluent-purple-light: #f3e8ff; + --fluent-shadow-sm: 0 2px 4px #0000000a, 0 2px 10px #0000000f; + --fluent-shadow-md: 0 4px 8px #0000000f, 0 4px 18px #00000014; + --fluent-shadow-lg: 0 8px 16px #00000014, 0 8px 32px #0000001a; + --fluent-shadow-xl: 0 20px 40px #0000001f, 0 10px 20px #00000014; + --fluent-sidebar-bg: #f3f3f3; + --fluent-sidebar-item-hover: #0000000a; + --fluent-sidebar-item-active: color-mix(in srgb, var(--fluent-primary) 6%, transparent); + --fluent-header-bg: var(--fluent-primary); + --fluent-header-text: #fff; + --fluent-input-border: #d1d1d1; + --fluent-input-border-bottom: #616161; + --fluent-input-border-hover: #c7c7c7; + --fluent-input-border-bottom-hover: #575757; + --fluent-input-border-bottom-focus: var(--fluent-primary); + --fluent-input-bg: #fff; + --fluent-input-bg-disabled: #f0f0f0; + --fluent-input-bg-hover: #f5f5f5; + --fluent-calendar-picker-invert: 0; + --fluent-btn-primary-bg: var(--fluent-primary); + --fluent-btn-primary-bg-hover: var(--fluent-primary-hover); + --fluent-btn-primary-bg-active: var(--fluent-primary-active); + --fluent-btn-secondary-bg: #fff; + --fluent-btn-secondary-bg-hover: #f5f5f5; + --fluent-btn-secondary-bg-active: #e0e0e0; + --fluent-btn-secondary-border: #d1d1d1; + --fluent-btn-secondary-border-hover: #d1d1d1; + --fluent-btn-secondary-border-active: #d1d1d1; + --fluent-card-bg: #fff; + --fluent-card-border: #e0e0e0; + --fluent-card-shadow: var(--fluent-shadow-sm); + --fluent-table-header-bg: #f5f5f5; + --fluent-table-row-hover: #f5f5f5; + --fluent-table-row-selected: var(--fluent-primary-lightest); + --fluent-table-border: #e0e0e0; + --fluent-modal-bg: #fff; + --fluent-modal-overlay: #0006; + --fluent-tab-indicator-color: var(--fluent-primary); + --fluent-checkbox-border: #616161; + --fluent-checkbox-bg: #fff; + --fluent-checkbox-checked-bg: var(--fluent-primary); + --fluent-checkbox-checked-border: var(--fluent-primary); + --fluent-checkbox-hover-border: #242424; + --fluent-scrollbar-thumb-bg: #c2c2c2; + --fluent-scrollbar-thumb-bg-hover: #a0a0a0; + --fluent-scrollbar-thumb-horizontal-bg: #00000026; + --fluent-scrollbar-thumb-horizontal-bg-hover: #00000059; + --fluent-progress-bg: #e0e0e0; + --fluent-progress-bar-bg: var(--fluent-primary); + --fluent-badge-bg: var(--fluent-primary); + --fluent-badge-text: #fff; + --fluent-toast-bg: #fff; + --fluent-toast-shadow: var(--fluent-shadow-lg); + --fluent-login-bg: #ffffffeb; + --fluent-login-border: #ffffffb3; + --fluent-login-icon-color: var(--fluent-text-secondary); + --fluent-tooltip-bg: #242424; + --fluent-tooltip-text: #fff; + --fluent-dropdown-bg: var(--fluent-bg-card); + --fluent-dropdown-border: var(--fluent-input-border); + --fluent-dropdown-shadow: 0 8px 16px #00000024, 0 0 2px #0000001f; + --fluent-dropdown-option-hover: var(--fluent-bg-hover); + --fluent-dropdown-option-selected: var(--fluent-primary-lightest); + --fluent-focus-color: #000; + --fluent-focus-inner-color: #fff; +} + +body[data-page^="admin-quickstart"] .network_container canvas, body[data-page^="admin-network_guide"] .network_container canvas, body[data-page^="admin-nas-raid"] .network_container canvas, body[data-page^="admin-network-interfaceconfig"] .network_container canvas { + filter: invert() hue-rotate(180deg) contrast(1.25) brightness(1.1); +} + +body[data-page^="admin-quickstart"] #actioner, body[data-page^="admin-network_guide"] #actioner, body[data-page^="admin-nas-raid"] #actioner, body[data-page^="admin-network-interfaceconfig"] #actioner { + z-index: 9999; +} + +body[data-page^="admin-quickstart"] #actioner form, body[data-page^="admin-network_guide"] #actioner form, body[data-page^="admin-nas-raid"] #actioner form, body[data-page^="admin-network-interfaceconfig"] #actioner form { + background-color: var(--fluent-card-bg) !important; +} + +body[data-page^="admin-quickstart"] #actioner > form, body[data-page^="admin-quickstart"] > #modal_overlay, body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent, body[data-page^="admin-network_guide"] #actioner > form, body[data-page^="admin-network_guide"] > #modal_overlay, body[data-page^="admin-network_guide"] > .main > .main-right > #maincontent, body[data-page^="admin-nas-raid"] #actioner > form, body[data-page^="admin-nas-raid"] > #modal_overlay, body[data-page^="admin-nas-raid"] > .main > .main-right > #maincontent, body[data-page^="admin-network-interfaceconfig"] #actioner > form, body[data-page^="admin-network-interfaceconfig"] > #modal_overlay, body[data-page^="admin-network-interfaceconfig"] > .main > .main-right > #maincontent { + --flow-bg-color: var(--fluent-card-bg); + --flow-span-color: var(--fluent-text-secondary); + --card-bg-color: var(--fluent-card-bg); + --card-box-shadow: var(--fluent-card-shadow); + --app-container_title-color: var(--fluent-text); + --app-container_status-label_block: var(--fluent-text-secondary); + --item-label_key-span-color: var(--fluent-text-secondary); + --item-label_value-span-color: var(--fluent-text); + --app-container_status-label_bg: var(--fluent-bg-hover); + --item_btn-border: 1px solid var(--fluent-primary); + --item_btn-color: var(--fluent-primary); + --tit-color: var(--fluent-text); + --popup-bg-color: var(--fluent-modal-bg); + --tag-bg-color: var(--fluent-bg-hover); + --gradient-bg-color: linear-gradient(90deg, transparent 0%, var(--fluent-card-bg) 62%, var(--fluent-card-bg) 100%); + --breadcrumbs-tit-color: var(--fluent-text-secondary); + --breadcrumbs-tit-color1: var(--fluent-text); + --border-color: var(--fluent-card-border); + --SystemUpdateBanner-bg-color: var(--fluent-bg-hover); + --SystemUpdateBanner-border-color: var(--fluent-card-border); + --btn-border-color: var(--fluent-border); + --card-txt-color: var(--fluent-text); + --SystemInfo-bg-color: var(--fluent-card-bg); +} + +[data-theme="dark"] body[data-page^="admin-network_guide"] .title { + color: var(--fluent-text) !important; +} + +[data-theme="dark"] body[data-page^="admin-network_guide"] .desc { + color: var(--fluent-text-secondary) !important; +} + +[data-theme="dark"] body[data-page^="admin-network_guide"] .title, [data-theme="dark"] body[data-page^="admin-network_guide"] .desc, [data-theme="dark"] body[data-page^="admin-network_guide"] code { + background-color: unset !important; +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent { + --lightningcss-light: initial; + --lightningcss-dark: ; + color-scheme: light; + --fluent-primary: #0078d4; + --fluent-primary-hover: var(--fluent-brand-70); + --fluent-primary-active: var(--fluent-brand-60); + --fluent-primary-light: var(--fluent-brand-140); + --fluent-primary-lighter: var(--fluent-brand-150); + --fluent-primary-lightest: var(--fluent-brand-160); + --fluent-selection-bg: var(--fluent-primary-light); + --fluent-selection-text: var(--fluent-text); + --fluent-white: #fff; + --fluent-black: #000; + --fluent-bg: #fff; + --fluent-bg-hover: #f5f5f5; + --fluent-bg-active: #ebebeb; + --fluent-bg-card: #fff; + --fluent-bg-page: #fafafa; + --fluent-text: #242424; + --fluent-text-secondary: #616161; + --fluent-text-disabled: #a0a0a0; + --fluent-text-on-primary: #fff; + --fluent-text-link: var(--fluent-primary); + --fluent-text-link-hover: var(--fluent-primary-hover); + --fluent-border: #d1d1d1; + --fluent-border-hover: #a0a0a0; + --fluent-border-active: var(--fluent-primary); + --fluent-success: #107c10; + --fluent-success-light: #f1faf1; + --fluent-warning: #ffb900; + --fluent-warning-light: #fff4ce; + --fluent-error: #d13438; + --fluent-error-hover: #b42b2f; + --fluent-error-active: #9a2226; + --fluent-error-light: #fde7e9; + --fluent-info: var(--fluent-primary); + --fluent-info-light: var(--fluent-primary-lightest); + --fluent-purple: #7e22ce; + --fluent-purple-light: #f3e8ff; + --fluent-shadow-sm: 0 2px 4px #0000000a, 0 2px 10px #0000000f; + --fluent-shadow-md: 0 4px 8px #0000000f, 0 4px 18px #00000014; + --fluent-shadow-lg: 0 8px 16px #00000014, 0 8px 32px #0000001a; + --fluent-shadow-xl: 0 20px 40px #0000001f, 0 10px 20px #00000014; + --fluent-sidebar-bg: #f3f3f3; + --fluent-sidebar-item-hover: #0000000a; + --fluent-sidebar-item-active: color-mix(in srgb, var(--fluent-primary) 6%, transparent); + --fluent-header-bg: var(--fluent-primary); + --fluent-header-text: #fff; + --fluent-input-border: #d1d1d1; + --fluent-input-border-bottom: #616161; + --fluent-input-border-hover: #c7c7c7; + --fluent-input-border-bottom-hover: #575757; + --fluent-input-border-bottom-focus: var(--fluent-primary); + --fluent-input-bg: #fff; + --fluent-input-bg-disabled: #f0f0f0; + --fluent-input-bg-hover: #f5f5f5; + --fluent-calendar-picker-invert: 0; + --fluent-btn-primary-bg: var(--fluent-primary); + --fluent-btn-primary-bg-hover: var(--fluent-primary-hover); + --fluent-btn-primary-bg-active: var(--fluent-primary-active); + --fluent-btn-secondary-bg: #fff; + --fluent-btn-secondary-bg-hover: #f5f5f5; + --fluent-btn-secondary-bg-active: #e0e0e0; + --fluent-btn-secondary-border: #d1d1d1; + --fluent-btn-secondary-border-hover: #d1d1d1; + --fluent-btn-secondary-border-active: #d1d1d1; + --fluent-card-bg: #fff; + --fluent-card-border: #e0e0e0; + --fluent-card-shadow: var(--fluent-shadow-sm); + --fluent-table-header-bg: #f5f5f5; + --fluent-table-row-hover: #f5f5f5; + --fluent-table-row-selected: var(--fluent-primary-lightest); + --fluent-table-border: #e0e0e0; + --fluent-modal-bg: #fff; + --fluent-modal-overlay: #0006; + --fluent-tab-indicator-color: var(--fluent-primary); + --fluent-checkbox-border: #616161; + --fluent-checkbox-bg: #fff; + --fluent-checkbox-checked-bg: var(--fluent-primary); + --fluent-checkbox-checked-border: var(--fluent-primary); + --fluent-checkbox-hover-border: #242424; + --fluent-scrollbar-thumb-bg: #c2c2c2; + --fluent-scrollbar-thumb-bg-hover: #a0a0a0; + --fluent-scrollbar-thumb-horizontal-bg: #00000026; + --fluent-scrollbar-thumb-horizontal-bg-hover: #00000059; + --fluent-progress-bg: #e0e0e0; + --fluent-progress-bar-bg: var(--fluent-primary); + --fluent-badge-bg: var(--fluent-primary); + --fluent-badge-text: #fff; + --fluent-toast-bg: #fff; + --fluent-toast-shadow: var(--fluent-shadow-lg); + --fluent-login-bg: #ffffffeb; + --fluent-login-border: #ffffffb3; + --fluent-login-icon-color: var(--fluent-text-secondary); + --fluent-tooltip-bg: #242424; + --fluent-tooltip-text: #fff; + --fluent-dropdown-bg: var(--fluent-bg-card); + --fluent-dropdown-border: var(--fluent-input-border); + --fluent-dropdown-shadow: 0 8px 16px #00000024, 0 0 2px #0000001f; + --fluent-dropdown-option-hover: var(--fluent-bg-hover); + --fluent-dropdown-option-selected: var(--fluent-primary-lightest); + --fluent-focus-color: #000; + --fluent-focus-inner-color: #fff; + --flow-bg-color: #d6d6d6; + --flow-span-color: #525252; + --card-bg-color: #d6d6d6; + --card-box-shadow: 0 0 10px 1px #0006; + --app-container_title-color: #000; + --app-container_status-label_block: #000; + --item-label_key-span-color: #525252; + --item-label_value-span-color: #000; + --app-container_status-label_bg: #ccc; + --item_btn-border: 1px solid #0078d4; + --item_btn-color: #0078d4; + --tit-color: #000; + --popup-bg-color: #d6d6d6; + --tag-bg-color: #ccc; + --gradient-bg-color: linear-gradient(90deg, #d6d6d600 0%, #d6d6d6cc 62%, #d6d6d6 100%); + --breadcrumbs-tit-color: #525252; + --breadcrumbs-tit-color1: #000; + --border-color: #c2c2c2; + --SystemUpdateBanner-bg-color: #eff6ff; + --SystemUpdateBanner-border-color: #bedbff; + --btn-border-color: #c2c2c2; + --card-txt-color: #000; + --SystemInfo-bg-color: #d6d6d6; + color: #000; + border-radius: var(--fluent-card-radius); + filter: invert() hue-rotate(180deg); + transition: filter var(--fluent-duration-normal) var(--fluent-easing-standard); + background-color: #e5e5e5; + --lightningcss-light: !important; + --lightningcss-dark: initial !important; + color-scheme: dark !important; +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form #main > div > .card-container > .feature-card > div > .header > div > svg, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form img, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form canvas, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form div.network-containers .model_btn, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay #main > div > .card-container > .feature-card > div > .header > div > svg, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay img, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay canvas, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay div.network-containers .model_btn, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent #main > div > .card-container > .feature-card > div > .header > div > svg, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent img, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent canvas, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent div.network-containers .model_btn { + filter: invert() hue-rotate(180deg); +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form #main > div > div:first-child > div:has( > a:first-child):has(svg) { + filter: invert() hue-rotate(180deg); +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay #main > div > div:first-child > div:has( > a:first-child):has(svg) { + filter: invert() hue-rotate(180deg); +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent #main > div > div:first-child > div:has( > a:first-child):has(svg) { + filter: invert() hue-rotate(180deg); +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .del-button, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .footer-btn, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .app-back, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .btn-pink, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .del-button, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .footer-btn, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .app-back, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .btn-pink, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .del-button, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .footer-btn, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .app-back, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .btn-pink { + filter: invert() hue-rotate(180deg) !important; + background: var(--fluent-error) !important; + color: var(--fluent-text-on-primary) !important; + border-color: var(--fluent-error) !important; +} + +[data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .cbi-button, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .modal-button.confirm, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .add-button, [data-theme="dark"] body[data-page^="admin-quickstart"] #actioner > form .app-next, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .cbi-button, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .modal-button.confirm, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .add-button, [data-theme="dark"] body[data-page^="admin-quickstart"] > #modal_overlay .app-next, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .cbi-button, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .modal-button.confirm, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .add-button, [data-theme="dark"] body[data-page^="admin-quickstart"] > .main > .main-right > #maincontent .app-next { + filter: invert() hue-rotate(180deg) !important; + background: var(--fluent-primary) !important; + color: var(--fluent-text-on-primary) !important; + border-color: var(--fluent-primary) !important; +} + +body.login-page { + width: 100vw !important; + min-width: 100vw !important; + max-width: 100vw !important; + margin: 0 !important; + overflow-x: hidden !important; +} + +.login-page { + box-sizing: border-box !important; + background: radial-gradient(circle, #f3f4f6 0%, #d1d5db 100%) !important; + flex-direction: column !important; + justify-content: flex-start !important; + align-items: center !important; + width: 100vw !important; + min-width: 100vw !important; + max-width: 100vw !important; + height: 100dvh !important; + min-height: 100dvh !important; + display: flex !important; + position: relative !important; + overflow: hidden !important; +} + +.login-page .main-bg { + z-index: 1 !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: cover !important; + width: 100% !important; + height: 100% !important; + position: absolute !important; + inset: 0 !important; +} + +.login-page .video { + background-color: var(--fluent-black) !important; + z-index: 1 !important; + justify-content: center !important; + align-items: center !important; + width: 100% !important; + height: 100% !important; + display: flex !important; + position: absolute !important; + inset: 0 !important; + overflow: hidden !important; +} + +.login-page .video video { + object-fit: cover !important; + width: 100% !important; + height: 100% !important; + position: absolute !important; + inset: 0 !important; + transform: none !important; +} + +.login-page .volume-control { + z-index: 5000 !important; + cursor: pointer !important; + opacity: .6 !important; + width: 24px !important; + height: 24px !important; + transition: opacity var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + background-image: url("./img/volume_high.svg") !important; + background-position: center !important; + background-repeat: no-repeat !important; + background-size: contain !important; + position: fixed !important; + inset-block-start: 20px !important; + inset-inline-end: 20px !important; +} + +.login-page .volume-control:hover { + opacity: 1 !important; +} + +.login-page .volume-control.mute { + background-image: url("./img/volume_off.svg") !important; +} + +.login-page .login-container { + -webkit-backdrop-filter: blur(var(--fluent-blur-radius)) !important; + top: auto !important; + width: 100% !important; + max-width: 488px !important; + height: auto !important; + box-shadow: none !important; + z-index: 2 !important; + box-sizing: border-box !important; + background: none !important; + flex-direction: column !important; + flex: auto !important; + justify-content: center !important; + align-items: center !important; + margin: 0 !important; + padding: 24px 24px 104px !important; + display: flex !important; + position: relative !important; + inset-inline-start: auto !important; + transform: none !important; +} + +.login-page .login-container .login-form { + will-change: transform,opacity; + animation: fluent-login-fade-in var(--fluent-duration-slow) var(--fluent-easing-easy-ease); + box-sizing: border-box !important; + background: color-mix(in srgb, var(--fluent-bg-card) calc(var(--fluent-blur-opacity) * 100%), transparent) !important; + border: 1px solid var(--fluent-border) !important; + border-radius: 12px !important; + flex-direction: column !important; + width: 100% !important; + min-height: 386px !important; + padding: 40px !important; + display: flex !important; + position: relative !important; + box-shadow: 0 0 2px #0000001f, 0 8px 16px #00000024 !important; +} + +.login-page .login-container .login-form .ms-card-header { + justify-content: center !important; + align-items: center !important; + width: 100% !important; + min-height: 48px !important; + margin-bottom: 28px !important; + padding-inline: 36px !important; + display: flex !important; + position: relative !important; +} + +.login-page .login-container .login-form .ms-back-button { + display: none; + inset-inline-start: -12px !important; + cursor: pointer !important; + color: var(--fluent-text) !important; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + background: none !important; + border: none !important; + border-radius: 50% !important; + justify-content: center !important; + align-items: center !important; + padding: 8px !important; + position: absolute !important; + top: 50% !important; + transform: translateY(-50%) !important; +} + +.login-page .login-container .login-form .ms-back-button svg { + width: 20px !important; + height: 24px !important; +} + +html[dir="rtl"] .login-page .login-container .login-form .ms-back-button svg, body[data-fluent-dir="rtl"] .login-page .login-container .login-form .ms-back-button svg { + transform: scaleX(-1); +} + +.login-page .login-container .login-form .ms-back-button:hover { + background-color: #80808026 !important; +} + +.login-page .login-container .login-form .ms-back-button:focus-visible { + outline: 2px solid var(--fluent-focus-color) !important; + background-color: #80808026 !important; +} + +.login-page .login-container .login-form .brand { + justify-content: center !important; + align-items: center !important; + gap: 10px !important; + text-decoration: none !important; + display: flex !important; +} + +.login-page .login-container .login-form .brand .icon { + width: 48px !important; + height: 48px !important; +} + +.login-page .login-container .login-form .brand .brand-text { + font-family: var(--fluent-font-family) !important; + font-size: 26px !important; + font-weight: var(--fluent-font-weight-bold, 700) !important; + color: var(--fluent-text-secondary) !important; + margin: 0 !important; + line-height: 1.1 !important; +} + +.login-page .login-container .login-form .login-title { + font-family: var(--fluent-font-family) !important; + font-size: 24px !important; + font-weight: var(--fluent-font-weight-semibold) !important; + color: var(--fluent-text) !important; + text-align: center !important; + background: none !important; + border: none !important; + margin: 0 0 10px !important; + padding: 0 !important; + line-height: 30px !important; +} + +.login-page .login-container .login-form .login-subtitle { + font-family: var(--fluent-font-family) !important; + color: var(--fluent-text-secondary) !important; + text-align: center !important; + margin: 0 0 24px !important; + font-size: 13px !important; + line-height: 18px !important; +} + +.login-page .login-container .login-form .form-login { + background: none !important; + grid-template-rows: 1fr !important; + grid-template-columns: 1fr !important; + width: 100% !important; + margin: 0 !important; + padding: 0 !important; + display: grid !important; + position: relative !important; + overflow: hidden !important; +} + +.login-page .login-container .login-form .form-login .step-container { + grid-area: 1 / 1 / -1 / -1 !important; + width: 100% !important; + transition: transform .35s cubic-bezier(.1, .9, .2, 1), opacity .25s cubic-bezier(.1, .9, .2, 1) !important; +} + +.login-page .login-container .login-form .form-login { + --login-step-enter-offset: 40px; + --login-step-exit-offset: -40px; +} + +html[dir="rtl"] .login-page .login-container .login-form .form-login, body[data-fluent-dir="rtl"] .login-page .login-container .login-form .form-login { + --login-step-enter-offset: -40px; + --login-step-exit-offset: 40px; +} + +.login-page .login-container .login-form .form-login #step-username { + opacity: 1 !important; + visibility: visible !important; + transform: translateX(0) !important; +} + +.login-page .login-container .login-form .form-login #step-password { + transform: translateX(var(--login-step-enter-offset)) !important; + opacity: 0 !important; + visibility: hidden !important; +} + +.login-page .login-container .login-form .form-login.show-step2 #step-username { + transform: translateX(var(--login-step-exit-offset)) !important; + opacity: 0 !important; + visibility: hidden !important; +} + +.login-page .login-container .login-form .form-login.show-step2 #step-password { + opacity: 1 !important; + visibility: visible !important; + transform: translateX(0) !important; +} + +.login-page .login-container .login-form .form-login.no-transition .step-container { + transition: none !important; +} + +.login-page .login-container .login-form .form-login .errorbox { + text-align: start !important; + box-shadow: none !important; + color: #e81123 !important; + font-family: var(--fluent-font-family) !important; + font-size: 12px !important; + font-weight: var(--fluent-font-weight-regular) !important; + box-sizing: border-box !important; + background: none !important; + border: none !important; + border-radius: 0 !important; + margin: 2px 0 0 !important; + padding: 0 !important; + padding-inline-start: 16px !important; + line-height: 16px !important; + display: block !important; + position: relative !important; +} + +.login-page .login-container .login-form .form-login .errorbox:before { + display: none !important; +} + +.login-page .login-container .login-form .form-login .errorbox:after { + content: "" !important; + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSI+CiAgPHBhdGggZD0iTTggMTVBNyA3IDAgMSAwIDggMWE3IDcgMCAwIDAgMCAxNHoiIGZpbGw9IiNlODExMjMiLz4KICA8cGF0aCBkPSJNOCA0YS43NS43NSAwIDAgMSAuNzUuNzV2NC41YS43NS43NSAwIDAgMS0xLjUgMHYtNC41QS43NS43NSAwIDAgMSA4IDR6bTAgOC4yNWEuODc1Ljg3NSAwIDEgMSAwLTEuNzUuODc1Ljg3NSAwIDAgMSAwIDEuNzV6IiBmaWxsPSIjZmZmZmZmIi8+Cjwvc3ZnPgo=) !important; + background-repeat: no-repeat !important; + background-size: contain !important; + flex-shrink: 0 !important; + width: 14px !important; + height: 14px !important; + display: block !important; + position: absolute !important; + inset-block-start: 1px !important; + inset-inline-start: 0 !important; +} + +.login-page .login-container .login-form .form-login .input-container { + flex-direction: column !important; + gap: 0 !important; + margin-bottom: 0 !important; + display: flex !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper { + width: 100% !important; + position: relative !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input { + box-sizing: border-box !important; + background-color: var(--fluent-input-bg) !important; + border: 1px solid var(--fluent-input-border) !important; + border-bottom: 1px solid var(--fluent-input-border) !important; + width: 100% !important; + height: 38px !important; + min-height: 38px !important; + font-family: var(--fluent-font-family) !important; + color: var(--fluent-text) !important; + box-shadow: none !important; + background-image: linear-gradient(0deg, var(--fluent-primary) 0%, var(--fluent-primary) 100%) !important; + transition: border-color var(--fluent-duration-fast) var(--fluent-easing-standard),background-color var(--fluent-duration-fast) var(--fluent-easing-standard),background-size var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + background-position: bottom !important; + background-repeat: no-repeat !important; + background-size: 0 2px !important; + border-radius: 4px !important; + outline: none !important; + margin: 0 !important; + padding: 0 10px !important; + font-size: 14px !important; + line-height: normal !important; + display: block !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input:hover:not(:focus):not(:disabled) { + border-color: var(--fluent-input-border-hover) !important; + border-bottom-color: var(--fluent-text) !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input:focus { + border-color: var(--fluent-input-border) !important; + border-bottom-color: var(--fluent-primary) !important; + box-shadow: 0 0 0 1px var(--fluent-primary) inset !important; + background-size: 100% 2px !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid { + background-image: linear-gradient(0deg, #e81123 0% 100%) !important; + background-size: 0 2px !important; + border-color: #e81123 !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid:hover:not(:focus):not(:disabled) { + border-color: #e81123 !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid:focus { + background-size: 100% 2px !important; + border-color: #e81123 !important; + box-shadow: inset 0 0 0 1px #e81123 !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-label { + inset-inline-start: 10px !important; + background-color: var(--fluent-bg-card) !important; + color: var(--fluent-text-secondary) !important; + font-family: var(--fluent-font-family) !important; + pointer-events: none !important; + transition: all var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + padding-inline: 4px !important; + font-size: 14px !important; + position: absolute !important; + top: 50% !important; + transform: translateY(-50%) !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input:focus ~ .ms-label, .login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input:not(:placeholder-shown) ~ .ms-label { + color: var(--fluent-primary) !important; + font-size: 12px !important; + top: 0 !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input:not(:focus):not(:placeholder-shown) ~ .ms-label { + color: var(--fluent-text-secondary) !important; +} + +.login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid:focus ~ .ms-label, .login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid:not(:placeholder-shown) ~ .ms-label { + color: #e81123 !important; +} + +.login-page .login-container .login-form .form-login .password-wrapper .ms-input { + padding-inline: 10px 36px !important; +} + +.login-page .login-container .login-form .form-login .ms-password-toggle { + border-start-start-radius: 0; + border-start-end-radius: 1px; + border-end-end-radius: 1px; + border-end-start-radius: 0; + cursor: pointer !important; + width: 32px !important; + color: var(--fluent-text-secondary) !important; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + background: none !important; + border: none !important; + outline: none !important; + justify-content: center !important; + align-items: center !important; + display: flex !important; + position: absolute !important; + inset-block: 1px 2px !important; + inset-inline-end: 1px !important; +} + +.login-page .login-container .login-form .form-login .ms-password-toggle:hover { + color: var(--fluent-text) !important; + background-color: #8080801a !important; +} + +.login-page .login-container .login-form .form-login .ms-password-toggle:focus-visible { + outline: 1px solid var(--fluent-primary) !important; + background-color: #8080801a !important; +} + +.login-page .login-container .login-form .form-login .ms-password-toggle svg { + color: inherit !important; + stroke: currentColor !important; + pointer-events: none !important; +} + +.login-page .login-container .login-form .button-container { + justify-content: center !important; + width: 100% !important; + margin-top: 20px !important; + display: flex !important; +} + +.login-page .login-container .login-form .ms-button { + background: var(--fluent-btn-primary-bg) !important; + border: 1px solid var(--fluent-btn-primary-bg) !important; + width: 100% !important; + height: 38px !important; + color: var(--fluent-text-on-primary) !important; + font-family: var(--fluent-font-family) !important; + text-align: center !important; + cursor: pointer !important; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + border-radius: 4px !important; + outline: none !important; + justify-content: center !important; + align-items: center !important; + padding: 8px 16px !important; + font-size: 14px !important; + font-weight: 400 !important; + line-height: 20px !important; + display: flex !important; +} + +.login-page .login-container .login-form .ms-button:hover { + background: var(--fluent-btn-primary-bg-hover) !important; + border-color: var(--fluent-btn-primary-bg-hover) !important; +} + +.login-page .login-container .login-form .ms-button:active { + background: var(--fluent-btn-primary-bg-active) !important; + border-color: var(--fluent-btn-primary-bg-active) !important; +} + +.login-page .login-container .login-form .ms-button:focus-visible { + outline: 2px solid var(--fluent-focus-color) !important; + outline-offset: 2px !important; +} + +.login-page .login-container .login-form .login-account-pill-wrapper { + justify-content: center !important; + width: 100% !important; + margin-bottom: 18px !important; + display: flex !important; +} + +.login-page .login-container .login-form .login-account-pill { + box-sizing: border-box !important; + max-width: 100% !important; + height: 32px !important; + color: var(--fluent-text) !important; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + background: none !important; + border: 1px solid #00000040 !important; + border-radius: 100px !important; + justify-content: center !important; + align-items: center !important; + padding: 0 12px !important; + font-size: 12px !important; + display: inline-flex !important; +} + +.login-page .login-container .login-form .login-account-pill:hover { + background-color: #0000000d !important; +} + +.login-page .login-container .login-form .login-account-pill__text { + text-overflow: ellipsis !important; + white-space: nowrap !important; + font-family: var(--fluent-font-family) !important; + color: var(--fluent-text) !important; + font-size: 13px !important; + font-weight: 400 !important; + line-height: 1.5 !important; + display: block !important; + overflow: hidden !important; +} + +.login-page footer { + text-align: center !important; + z-index: 10 !important; + width: 100% !important; + box-shadow: none !important; + background: none !important; + border: none !important; + margin: 0 !important; + padding: 0 24px !important; + position: fixed !important; + inset-block-end: 18px !important; + inset-inline: 0 !important; +} + +.login-page footer div { + color: var(--fluent-text-secondary) !important; + opacity: .75 !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: center !important; + gap: 16px !important; + width: auto !important; + max-width: 100% !important; + font-size: 12px !important; + display: inline-flex !important; +} + +.login-page footer a.luci-link { + color: var(--fluent-text-secondary) !important; + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard) !important; + font-weight: normal !important; + text-decoration: none !important; +} + +.login-page footer a.luci-link:hover { + color: var(--fluent-text) !important; + text-decoration: underline !important; +} + +[data-theme="dark"] .login-page { + background: radial-gradient(circle, #171c26 0%, #0b0e14 100%) !important; +} + +[data-theme="dark"] .login-page .login-container { + -webkit-backdrop-filter: blur(var(--fluent-blur-radius-dark)) !important; +} + +[data-theme="dark"] .login-page .login-container .login-form { + background: color-mix(in srgb, var(--fluent-bg-card) calc(var(--fluent-blur-opacity-dark) * 100%), transparent) !important; + border-color: #ffffff1a !important; + box-shadow: 0 0 2px #0000003d, 0 8px 16px #00000047 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .brand .brand-text { + color: #a3a3a3 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .errorbox { + color: #e37d80 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .errorbox:after { + background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSI+CiAgPHBhdGggZD0iTTggMTVBNyA3IDAgMSAwIDggMWE3IDcgMCAwIDAgMCAxNHoiIGZpbGw9IiNlMzdkODAiLz4KICA8cGF0aCBkPSJNOCA0YS43NS43NSAwIDAgMSAuNzUuNzV2NC41YS43NS43NSAwIDAgMS0xLjUgMHYtNC41QS43NS43NSAwIDAgMSA4IDR6bTAgOC4yNWEuODc1Ljg3NSAwIDEgMSAwLTEuNzUuODc1Ljg3NSAwIDAgMSAwIDEuNzV6IiBmaWxsPSIjMjkyOTI5Ii8+Cjwvc3ZnPgo=) !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid { + background-image: linear-gradient(0deg, #e37d80 0% 100%) !important; + background-size: 0 2px !important; + border-color: #e37d80 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid:hover:not(:focus):not(:disabled) { + border-color: #e37d80 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .ms-input-wrapper .ms-input.cbi-input-invalid:focus { + background-size: 100% 2px !important; + border-color: #e37d80 !important; + box-shadow: inset 0 0 0 1px #e37d80 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .login-account-pill { + border-color: #ffffff40 !important; +} + +[data-theme="dark"] .login-page .login-container .login-form .form-login .login-account-pill:hover { + background-color: #ffffff1a !important; +} + +@media (max-width: 575px) { + .login-page { + justify-content: flex-start !important; + align-items: stretch !important; + padding: 0 !important; + } + + .login-page .login-container { + justify-content: flex-start !important; + align-items: stretch !important; + max-width: 100% !important; + min-height: 100dvh !important; + padding: 0 0 112px !important; + } + + .login-page .login-container .login-form { + min-height: auto !important; + box-shadow: none !important; + background: var(--fluent-bg-card) !important; + border: none !important; + border-radius: 0 !important; + padding: 22px 24px 24px !important; + } + + .login-page .login-container .login-form .ms-card-header { + margin-bottom: 24px !important; + } + + .login-page .login-container .login-form .login-account-pill-wrapper { + margin-bottom: 12px !important; + } + + .login-page .login-container .login-form .login-title { + margin-bottom: 12px !important; + font-size: 20px !important; + line-height: 26px !important; + } + + .login-page .login-container .login-form .login-subtitle { + margin-bottom: 20px !important; + } + + .login-page .login-container .login-form .form-login .input-container { + gap: 0 !important; + margin-bottom: 0 !important; + } + + .login-page footer { + padding: 0 16px !important; + bottom: 12px !important; + } + + .login-page footer div { + flex-direction: column !important; + gap: 6px !important; + width: 100% !important; + display: flex !important; + } +} + +.main-left .nav > li > a:first-child[data-title="Dashboard"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Dashboard"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M8.998 2.388a1.5 1.5 0 0 1 2.005 0l5.5 4.942A1.5 1.5 0 0 1 17 8.445V15.5a1.5 1.5 0 0 1-1.5 1.5H13a1.5 1.5 0 0 1-1.5-1.5V12a.5.5 0 0 0-.5-.5H9a.5.5 0 0 0-.5.5v3.5A1.5 1.5 0 0 1 7 17H4.5A1.5 1.5 0 0 1 3 15.5V8.445c0-.425.18-.83.498-1.115zm1.336.744a.5.5 0 0 0-.668 0l-5.5 4.942A.5.5 0 0 0 4 8.445V15.5a.5.5 0 0 0 .5.5H7a.5.5 0 0 0 .5-.5V12A1.5 1.5 0 0 1 9 10.5h2a1.5 1.5 0 0 1 1.5 1.5v3.5a.5.5 0 0 0 .5.5h2.5a.5.5 0 0 0 .5-.5V8.445a.5.5 0 0 0-.166-.371z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Dashboard"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Dashboard"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M11.003 2.388a1.5 1.5 0 0 0-2.005 0l-5.5 4.942A1.5 1.5 0 0 0 3 8.445V15.5A1.5 1.5 0 0 0 4.5 17h2A1.5 1.5 0 0 0 8 15.5v-4a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 0 1.5 1.5h2a1.5 1.5 0 0 0 1.5-1.5V8.445a1.5 1.5 0 0 0-.497-1.115z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Dashboard"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Fluent Dashboard"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Fluent Dashboard"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M8.998 2.388a1.5 1.5 0 0 1 2.005 0l5.5 4.942A1.5 1.5 0 0 1 17 8.445V15.5a1.5 1.5 0 0 1-1.5 1.5H13a1.5 1.5 0 0 1-1.5-1.5V12a.5.5 0 0 0-.5-.5H9a.5.5 0 0 0-.5.5v3.5A1.5 1.5 0 0 1 7 17H4.5A1.5 1.5 0 0 1 3 15.5V8.445c0-.425.18-.83.498-1.115zm1.336.744a.5.5 0 0 0-.668 0l-5.5 4.942A.5.5 0 0 0 4 8.445V15.5a.5.5 0 0 0 .5.5H7a.5.5 0 0 0 .5-.5V12A1.5 1.5 0 0 1 9 10.5h2a1.5 1.5 0 0 1 1.5 1.5v3.5a.5.5 0 0 0 .5.5h2.5a.5.5 0 0 0 .5-.5V8.445a.5.5 0 0 0-.166-.371z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Fluent Dashboard"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Fluent Dashboard"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M11.003 2.388a1.5 1.5 0 0 0-2.005 0l-5.5 4.942A1.5 1.5 0 0 0 3 8.445V15.5A1.5 1.5 0 0 0 4.5 17h2A1.5 1.5 0 0 0 8 15.5v-4a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 0 1.5 1.5h2a1.5 1.5 0 0 0 1.5-1.5V8.445a1.5 1.5 0 0 0-.497-1.115z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Fluent Dashboard"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="QuickStart"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="QuickStart"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M6.19 2.77c.131-.456.548-.77 1.022-.77h5.25c.725 0 1.237.71 1.007 1.398l-.002.008L12.205 7h2.564c.947 0 1.407 1.144.767 1.811l-.004.004l-8.676 8.858c-.755.782-2.06.06-1.796-.996l1.17-4.679H4.963a1.062 1.062 0 0 1-1.022-1.354zM7.213 3a.06.06 0 0 0-.06.045l-2.25 7.874c-.01.04.02.08.06.08H6.87a.5.5 0 0 1 .485.62l-1.325 5.3a.1.1 0 0 0-.003.03q0 .006.003.011q.005.013.03.03c.018.01.034.01.042.01l.01-.004a.1.1 0 0 0 .024-.018l.004-.004l8.675-8.856a.06.06 0 0 0 .017-.032a.1.1 0 0 0-.007-.044a.1.1 0 0 0-.025-.034q-.006-.007-.03-.008H11.5a.5.5 0 0 1-.472-.666l1.493-4.254a.062.062 0 0 0-.06-.08z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="QuickStart"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="QuickStart"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.212 2c-.474 0-.89.314-1.021.77l-2.25 7.874a1.062 1.062 0 0 0 1.022 1.354H6.23l-1.17 4.68c-.264 1.055 1.041 1.777 1.796.995l8.676-8.858l.004-.004c.64-.667.18-1.811-.767-1.811h-2.564l1.262-3.594l.002-.008A1.062 1.062 0 0 0 12.461 2z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="QuickStart"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Home"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Home"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2.5 5.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m15.75-.75a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m-8.046-.706a.5.5 0 0 0-.408 0h-.002l-.003.002l-.012.005l-.04.02l-.151.07a14.6 14.6 0 0 0-2.13 1.271c-.584.423-1.187.939-1.648 1.528C5.35 7.527 5 8.225 5 9c0 .711.148 1.387.416 2H3V6.5a.5.5 0 0 0-1 0V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-1 0V11h-2.416A5 5 0 0 0 15 9c0-.775-.35-1.473-.81-2.06c-.461-.589-1.064-1.105-1.647-1.527a14.6 14.6 0 0 0-2.281-1.343l-.041-.019l-.012-.005l-.003-.002zM17 17h-5v-2a2 2 0 1 0-4 0v2H3v-5h14zm-7-3a1 1 0 0 1 1 1v2H9v-2a1 1 0 0 1 1-1m3.403-6.443c.384.49.597.975.597 1.443c0 .729-.195 1.412-.535 2h-6.93A4 4 0 0 1 6 9c0-.468.213-.953.597-1.443c.382-.488.904-.941 1.446-1.334A13.6 13.6 0 0 1 10 5.053a13.6 13.6 0 0 1 1.957 1.17c.542.393 1.064.846 1.446 1.334'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Home"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Home"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3.25 4.75a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m14.25.75a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m-7.296-1.456a.5.5 0 0 0-.408 0h-.002l-.003.002l-.012.005l-.04.02l-.151.07a14.6 14.6 0 0 0-2.13 1.271c-.584.423-1.187.939-1.648 1.528C5.35 7.527 5 8.225 5 9c0 .711.148 1.387.416 2H3V6.5a.5.5 0 0 0-1 0V17a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1a2 2 0 1 1 4 0v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1V6.5a.5.5 0 0 0-1 0V11h-2.416A5 5 0 0 0 15 9c0-.775-.35-1.473-.81-2.06c-.461-.589-1.064-1.105-1.647-1.527a14.6 14.6 0 0 0-2.281-1.343l-.041-.019l-.012-.005l-.003-.002z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Home"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="NetworkGuide"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="NetworkGuide"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3 10a7 7 0 1 1 14 0a7 7 0 0 1-14 0m7-8a8 8 0 1 0 0 16a8 8 0 0 0 0-16M6.097 7.42c-.383-.84.482-1.706 1.324-1.323l3.262 1.482a3.5 3.5 0 0 1 1.738 1.738l1.482 3.262c.383.841-.483 1.707-1.324 1.324l-3.262-1.482a3.5 3.5 0 0 1-1.738-1.738zm4.172 1.07L7.007 7.007l1.483 3.262a2.5 2.5 0 0 0 1.24 1.241l3.262 1.483l-1.483-3.262A2.5 2.5 0 0 0 10.27 8.49'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="NetworkGuide"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="NetworkGuide"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16M7.316 6.323c-.632-.291-1.286.363-.995.995l1.502 3.264c.324.704.89 1.269 1.593 1.593l3.265 1.502c.632.29 1.286-.363.995-.995l-1.503-3.263a3.25 3.25 0 0 0-1.593-1.593z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="NetworkGuide"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Status"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Status"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.497 16.803L15.253 12H14.25q-.184 0-.36-.044l-3.865 3.902l-3.863-3.902Q5.986 12 5.8 12H4.799l4.755 4.803c.26.263.682.263.942 0m6.227-12.49a4.42 4.42 0 0 1 .978 4.7A2 2 0 0 0 17.5 9h-.889a3.415 3.415 0 0 0-.598-3.984A3.306 3.306 0 0 0 11.3 5l-.951.963a.5.5 0 0 1-.711 0l-.96-.97a3.3 3.3 0 0 0-4.706-.016C2.899 6.061 2.713 7.711 3.42 9H2.5q-.09 0-.18.01a4.4 4.4 0 0 1 .941-4.736a4.3 4.3 0 0 1 6.127.016l.605.61l.596-.603l.109-.106a4.306 4.306 0 0 1 6.026.121M7.962 6.307a.5.5 0 0 0-.922-.004L5.47 10H2.5a.5.5 0 0 0 0 1h3.3a.5.5 0 0 0 .46-.304l1.235-2.907l2.043 4.903a.5.5 0 0 0 .886.073l2.143-3.429l1.307 1.493a.5.5 0 0 0 .376.171h3.25a.5.5 0 0 0 0-1h-3.023l-1.6-1.83a.5.5 0 0 0-.801.065l-1.987 3.179z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Status"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Status"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.388 4.29a4.3 4.3 0 0 0-6.127-.016a4.4 4.4 0 0 0-.94 4.737Q2.408 9 2.5 9h2.308L6.12 5.913a1.5 1.5 0 0 1 2.765.01l1.383 3.319l.96-1.537a1.5 1.5 0 0 1 2.4-.193L14.932 9H17.5q.103 0 .202.014a4.42 4.42 0 0 0-.978-4.702a4.305 4.305 0 0 0-6.135-.015l-.596.603zm.166 12.513L4.799 12H5.8a1.5 1.5 0 0 0 1.38-.913l.306-.72l1.13 2.71a1.5 1.5 0 0 0 2.656.218l1.43-2.287l.42.48A1.5 1.5 0 0 0 14.25 12h1.004l-4.757 4.803a.66.66 0 0 1-.943 0M7.962 6.308a.5.5 0 0 0-.922-.004L5.47 10H2.5a.5.5 0 0 0 0 1h3.3a.5.5 0 0 0 .46-.304l1.235-2.907l2.043 4.903a.5.5 0 0 0 .886.073l2.143-3.429l1.307 1.493a.5.5 0 0 0 .376.171h3.25a.5.5 0 0 0 0-1h-3.023l-1.6-1.83a.5.5 0 0 0-.801.065l-1.987 3.179z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Status"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="System"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="System"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M1.911 7.383a8.5 8.5 0 0 1 1.78-3.08a.5.5 0 0 1 .54-.135l1.918.686a1 1 0 0 0 1.32-.762l.366-2.006a.5.5 0 0 1 .388-.4a8.5 8.5 0 0 1 3.554 0a.5.5 0 0 1 .388.4l.366 2.006a1 1 0 0 0 1.32.762l1.919-.686a.5.5 0 0 1 .54.136a8.5 8.5 0 0 1 1.78 3.079a.5.5 0 0 1-.153.535l-1.555 1.32a1 1 0 0 0 0 1.524l1.555 1.32a.5.5 0 0 1 .152.535a8.5 8.5 0 0 1-1.78 3.08a.5.5 0 0 1-.54.135l-1.918-.686a1 1 0 0 0-1.32.762l-.366 2.007a.5.5 0 0 1-.388.399a8.5 8.5 0 0 1-3.554 0a.5.5 0 0 1-.388-.4l-.366-2.006a1 1 0 0 0-1.32-.762l-1.918.686a.5.5 0 0 1-.54-.136a8.5 8.5 0 0 1-1.78-3.079a.5.5 0 0 1 .152-.535l1.555-1.32a1 1 0 0 0 0-1.524l-1.555-1.32a.5.5 0 0 1-.152-.535m1.06-.006l1.294 1.098a2 2 0 0 1 0 3.05l-1.293 1.098c.292.782.713 1.51 1.244 2.152l1.596-.57q.155-.055.315-.085a2 2 0 0 1 2.326 1.609l.304 1.669a7.6 7.6 0 0 0 2.486 0l.304-1.67a1.998 1.998 0 0 1 2.641-1.524l1.596.571a7.5 7.5 0 0 0 1.245-2.152l-1.294-1.098a1.998 1.998 0 0 1 0-3.05l1.294-1.098a7.5 7.5 0 0 0-1.245-2.152l-1.596.57a2 2 0 0 1-2.64-1.524l-.305-1.669a7.6 7.6 0 0 0-2.486 0l-.304 1.669a2 2 0 0 1-2.64 1.525l-1.597-.571a7.5 7.5 0 0 0-1.244 2.152M7.502 10a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m1 0a1.5 1.5 0 1 0 3 0a1.5 1.5 0 0 0-3 0'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="System"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="System"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M1.911 7.383a8.5 8.5 0 0 1 1.78-3.08a.5.5 0 0 1 .54-.135l1.918.686a1 1 0 0 0 1.32-.762l.366-2.006a.5.5 0 0 1 .388-.4a8.5 8.5 0 0 1 3.554 0a.5.5 0 0 1 .388.4l.366 2.006a1 1 0 0 0 1.32.762l1.919-.686a.5.5 0 0 1 .54.136a8.5 8.5 0 0 1 1.78 3.079a.5.5 0 0 1-.153.535l-1.555 1.32a1 1 0 0 0 0 1.524l1.555 1.32a.5.5 0 0 1 .152.535a8.5 8.5 0 0 1-1.78 3.08a.5.5 0 0 1-.54.135l-1.918-.686a1 1 0 0 0-1.32.762l-.366 2.007a.5.5 0 0 1-.388.399a8.5 8.5 0 0 1-3.554 0a.5.5 0 0 1-.388-.4l-.366-2.006a1 1 0 0 0-1.32-.762l-1.918.686a.5.5 0 0 1-.54-.136a8.5 8.5 0 0 1-1.78-3.079a.5.5 0 0 1 .152-.535l1.555-1.32a1 1 0 0 0 0-1.524l-1.555-1.32a.5.5 0 0 1-.152-.535M8.001 10a2 2 0 1 0 4 0a2 2 0 0 0-4 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="System"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Services"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Services"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01zm5.122-.826a.5.5 0 0 0-.645-.053l-.068.06l-2.616 2.713a.5.5 0 0 0-.057.623l.063.078l2.616 2.615a.5.5 0 0 0 .62.07l.078-.061l2.758-2.627a.5.5 0 0 0 .054-.638l-.059-.069z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Services"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Services"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Services"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="iStore"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="iStore"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3E%3Cpath fill='currentColor' d='M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3zm0 1.5H6.25A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.784 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75V6.25a1.75 1.75 0 0 0-1.75-1.75M9.369 15.254l-.51.872a.75.75 0 0 1-1.34-.665l.045-.091l.067-.116zm3.924-5.23l1.719 2.98h1.74a.75.75 0 0 1 .743.65l.007.1a.75.75 0 0 1-.648.744l-.102.007h-.875l.502.87a.75.75 0 0 1-1.243.834l-.056-.085l-2.658-4.608zm-.03-2.923a.75.75 0 0 1 .315.935l-.046.091l-2.85 4.877h1.732l.865 1.5H7.252a.75.75 0 0 1-.102-1.492l.102-.007h1.694l2.18-3.734l-.662-1.147a.75.75 0 0 1 .19-.968l.085-.057a.75.75 0 0 1 .968.19l.056.085l.233.406l.241-.41a.75.75 0 0 1 1.026-.269'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="iStore"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="iStore"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none'%3E%3Cpath fill='currentColor' d='M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3zM9.369 15.254H7.63l-.067.116l-.045.091a.75.75 0 0 0 1.283.75l.057-.085zm3.924-5.23l-.871 1.492l2.658 4.608l.056.085a.75.75 0 0 0 1.288-.743l-.045-.092l-.502-.87h.875l.102-.006a.75.75 0 0 0 .641-.641l.007-.102l-.007-.102a.75.75 0 0 0-.641-.641l-.102-.007h-1.74zm-.03-2.923a.75.75 0 0 0-.969.185l-.057.085l-.24.409l-.234-.406l-.056-.084a.75.75 0 0 0-.867-.24l-.101.05l-.085.056a.75.75 0 0 0-.239.867l.05.1l.662 1.148l-2.181 3.734H7.252l-.102.007a.75.75 0 0 0 0 1.486l.102.007h6.027l-.865-1.5l-1.731-.001l2.85-4.877l.045-.091a.75.75 0 0 0-.315-.935'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="iStore"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Docker"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Docker"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M11.3 2.48a3.5 3.5 0 0 0-2.6 0L2.943 4.785A1.5 1.5 0 0 0 2 6.176v7.646a1.5 1.5 0 0 0 .943 1.393L8.7 17.518a3.5 3.5 0 0 0 2.6 0l5.757-2.303A1.5 1.5 0 0 0 18 13.822V6.176a1.5 1.5 0 0 0-.943-1.392zm-2.228.93a2.5 2.5 0 0 1 1.857 0l5.225 2.09l-2.279.91l-6.154-2.46zM6.375 4.487l6.154 2.461L10 7.961L3.846 5.499zm4.125 4.35l6.5-2.6v7.584a.5.5 0 0 1-.314.465l-5.757 2.303q-.21.083-.429.128zm-1 0v7.88a2.5 2.5 0 0 1-.428-.128l-5.758-2.303A.5.5 0 0 1 3 13.822V6.238z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Docker"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Docker"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='m10 7.96l3.029-1.21l-7.5-3l-2.586 1.034a1.5 1.5 0 0 0-.364.208zM2.035 5.853A1.5 1.5 0 0 0 2 6.176v7.646a1.5 1.5 0 0 0 .943 1.393L8.7 17.518q.391.156.8.214V8.838zm8.465 11.88a3.5 3.5 0 0 0 .8-.214l5.757-2.303A1.5 1.5 0 0 0 18 13.822V6.176q0-.166-.035-.324L10.5 8.838zm6.921-12.74l-3.046 1.219l-7.5-3L8.7 2.48a3.5 3.5 0 0 1 2.6 0l5.757 2.303a1.5 1.5 0 0 1 .364.208'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Docker"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="NAS"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="NAS"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4 5c0-1.007.875-1.755 1.904-2.223C6.978 2.289 8.427 2 10 2s3.022.289 4.096.777C15.125 3.245 16 3.993 16 5v10c0 1.007-.875 1.755-1.904 2.223C13.022 17.71 11.573 18 10 18s-3.022-.289-4.096-.777C4.875 16.755 4 16.007 4 15zm1 0c0 .374.356.875 1.318 1.313C7.234 6.729 8.536 7 10 7s2.766-.27 3.682-.687C14.644 5.875 15 5.373 15 5c0-.374-.356-.875-1.318-1.313C12.766 3.271 11.464 3 10 3s-2.766.27-3.682.687C5.356 4.125 5 4.627 5 5m10 1.698a5 5 0 0 1-.904.525C13.022 7.711 11.573 8 10 8s-3.022-.289-4.096-.777A5 5 0 0 1 5 6.698V15c0 .374.356.875 1.318 1.313c.916.416 2.218.687 3.682.687s2.766-.27 3.682-.687C14.644 15.875 15 15.373 15 15z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="NAS"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="NAS"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 8c3.314 0 6-1.343 6-3s-2.686-3-6-3s-6 1.343-6 3s2.686 3 6 3m4.69.016c.47-.235.927-.534 1.31-.896V15c0 1.657-2.686 3-6 3s-6-1.343-6-3V7.12c.383.362.84.661 1.31.896C6.562 8.642 8.222 9 10 9s3.438-.358 4.69-.984'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="NAS"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="VPN"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="VPN"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.341a3 3 0 0 0-1-.94V5.428a16 16 0 0 1-5.6-2.082l-.4-.249l-.4.249A16 16 0 0 1 4 5.428V9.5c0 3.392 1.968 5.863 6 7.463V17c0 .342.086.663.237.945l-.057.022a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421M12.5 12v-1a2 2 0 1 1 4 0v1h.5a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1zm1-1v1h2v-1a1 1 0 1 0-2 0m1.75 4a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="VPN"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="VPN"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.5 12v-1a2 2 0 1 1 4 0v1h.5a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1zm1-1v1h2v-1a1 1 0 1 0-2 0m1.75 4a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.341A3 3 0 0 0 11.5 11v.063A2 2 0 0 0 10 13v4c0 .342.086.663.237.945l-.057.022a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="VPN"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Network"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Network"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3.5 9.5a6.5 6.5 0 0 1 13 0a.5.5 0 0 0 1 0a7.5 7.5 0 0 0-15 0a.5.5 0 0 0 1 0m6.5-4a4 4 0 0 0-4 4a.5.5 0 0 1-1 0a5 5 0 0 1 10 0a.5.5 0 0 1-1 0a4 4 0 0 0-4-4M7.75 9.25a2.25 2.25 0 1 1 2.75 2.194V13h4a2.5 2.5 0 0 1 0 5h-9a2.5 2.5 0 0 1 0-5h4v-1.556A2.25 2.25 0 0 1 7.75 9.25M10 8a1.25 1.25 0 1 0 0 2.5A1.25 1.25 0 0 0 10 8m-4.5 6a1.5 1.5 0 0 0 0 3h9a1.5 1.5 0 0 0 0-3z'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Network"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Network"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3.5 9.5a6.5 6.5 0 0 1 13 0a.5.5 0 0 0 1 0a7.5 7.5 0 0 0-15 0a.5.5 0 0 0 1 0m6.5-4a4 4 0 0 0-4 4a.5.5 0 0 1-1 0a5 5 0 0 1 10 0a.5.5 0 0 1-1 0a4 4 0 0 0-4-4M7.75 9.25a2.25 2.25 0 1 1 2.75 2.194V13h4a2.5 2.5 0 0 1 0 5h-9a2.5 2.5 0 0 1 0-5h4v-1.556A2.25 2.25 0 0 1 7.75 9.25'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Network"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Statistics"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Statistics"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2.5 2a.5.5 0 0 1 .5.5v13A1.5 1.5 0 0 0 4.5 17h13a.5.5 0 0 1 0 1h-13A2.5 2.5 0 0 1 2 15.5v-13a.5.5 0 0 1 .5-.5M12 5.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 .5.5v4a.5.5 0 0 1-1 0V6.707l-4.646 4.647a.5.5 0 0 1-.708 0L9 9.707l-3.146 3.147a.5.5 0 0 1-.708-.708l3.5-3.5a.5.5 0 0 1 .708 0L11 10.293L15.293 6H12.5a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Statistics"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Statistics"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2.75 2a.75.75 0 0 1 .75.75v12.5c0 .69.56 1.25 1.25 1.25h12.5a.75.75 0 0 1 0 1.5H4.75A2.75 2.75 0 0 1 2 15.25V2.75A.75.75 0 0 1 2.75 2M12 5.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0V7.56l-3.97 3.97a.75.75 0 0 1-1.06 0L9 10.06l-2.22 2.22a.75.75 0 0 1-1.06-1.06l2.75-2.75a.75.75 0 0 1 1.06 0L11 9.94l3.44-3.44h-1.69a.75.75 0 0 1-.75-.75'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Statistics"] li a .menu-icon { + display: inline-flex; +} + +.main-left .nav > li > a:first-child[data-title="Log_out"] .menu-icon:before, .main-left .nav li.slide .slide-menu[data-parent="Log_out"] li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.5 17a.5.5 0 0 0 0-1H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6.5a.5.5 0 0 0 0-1H6a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3zm1.146-10.854a.5.5 0 0 1 .708 0l3.5 3.5a.5.5 0 0 1 0 .708l-3.5 3.5a.5.5 0 0 1-.708-.708l2.647-2.646H7.5a.5.5 0 0 1 0-1h8.793l-2.647-2.646a.5.5 0 0 1 0-.708'/%3E%3C/svg%3E"); +} + +.main-left .nav > li > a:first-child[data-title="Log_out"] .menu-icon:after, .main-left .nav li.slide .slide-menu[data-parent="Log_out"] li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.25 17.5a.75.75 0 0 0 0-1.5H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h6.25a.75.75 0 0 0 0-1.5H6A3.5 3.5 0 0 0 2.5 6v8A3.5 3.5 0 0 0 6 17.5zm1.491-11.301a.75.75 0 0 1 1.06.042l3 3.25a.75.75 0 0 1 0 1.018l-3 3.25A.75.75 0 1 1 13.7 12.74l1.838-1.991H7.75a.75.75 0 0 1 0-1.5h7.787l-1.838-1.991a.75.75 0 0 1 .042-1.06'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu[data-parent="Log_out"] li a .menu-icon { + display: inline-flex; +} + +.main-left { + top: var(--fluent-header-height, 48px); + width: var(--fluent-sidebar-width, 260px); + height: calc(100dvh - var(--fluent-header-height, 48px)); + background: var(--fluent-sidebar-bg); + border-inline-end: 1px solid var(--fluent-border); + z-index: var(--fluent-zindex-menu); + transition: width var(--fluent-duration-normal) var(--fluent-easing-standard),transform var(--fluent-duration-normal) var(--fluent-easing-standard); + position: fixed; + inset-inline-start: 0; + overflow: hidden auto; +} + +.main-left::-webkit-scrollbar { + width: var(--fluent-scrollbar-width); + height: var(--fluent-scrollbar-width); +} + +.main-left::-webkit-scrollbar:horizontal { + height: var(--fluent-scrollbar-horizontal-height, 5px); +} + +.main-left::-webkit-scrollbar-track { + background: var(--fluent-scrollbar-track-bg); +} + +.main-left::-webkit-scrollbar-thumb { + background: var(--fluent-scrollbar-thumb-bg); + border-radius: 3px; +} + +.main-left::-webkit-scrollbar-thumb:hover { + background: var(--fluent-scrollbar-thumb-bg-hover); +} + +.main-left::-webkit-scrollbar-thumb:horizontal { + background: var(--fluent-scrollbar-thumb-horizontal-bg); + border-radius: var(--fluent-radius-circular, 10000px); +} + +.main-left::-webkit-scrollbar-thumb:horizontal:hover { + background: var(--fluent-scrollbar-thumb-horizontal-bg-hover); +} + +.main-left::-webkit-scrollbar-corner { + background: none; + display: none; +} + +.main-left::-webkit-resizer { + background: none; +} + +.main-left .sidebar__search-slot { + padding: var(--fluent-space-sm) var(--fluent-space-sm) 0; + margin-bottom: var(--fluent-space-xs); + flex-shrink: 0; + display: none; +} + +.main-right { + height: 100dvh; + padding-top: var(--fluent-header-height, 48px); + transition: margin-inline-start var(--fluent-duration-normal) var(--fluent-easing-standard); + flex-direction: column; + margin-inline-start: var(--fluent-sidebar-width, 260px); + display: flex; + overflow: hidden; +} + +.main-left .nav { + padding: var(--fluent-space-xs) var(--fluent-space-sm) var(--fluent-space-sm); + margin: 0; + list-style: none; + position: relative; +} + +.main-left .nav .fluent-sidebar-parent-slider { + background-color: var(--fluent-primary); + width: 3px; + transition: top var(--fluent-duration-normal) var(--fluent-easing-standard),height var(--fluent-duration-normal) var(--fluent-easing-standard),opacity var(--fluent-duration-normal) var(--fluent-easing-standard); + pointer-events: none; + z-index: 10; + border-radius: 999px; + position: absolute; + inset-inline-start: 3px; +} + +body[data-tab-animation="0"] .main-left .nav .fluent-sidebar-parent-slider, body[data-reduce-motion="true"] .main-left .nav .fluent-sidebar-parent-slider { + transition: none !important; +} + +.main-left .nav > li > a:first-child { + align-items: center; + gap: var(--fluent-space-sm); + padding-block: 0; + padding-inline: 6px var(--fluent-space-sm); + border-radius: var(--fluent-radius-md); + cursor: pointer; + min-width: 0; + height: 36px; + font-size: var(--fluent-font-size-base); + font-weight: var(--fluent-font-weight-regular); + color: var(--fluent-text); + box-shadow: none; + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard),color var(--fluent-duration-fast) var(--fluent-easing-standard),border-color var(--fluent-duration-fast) var(--fluent-easing-standard); + background: none; + margin: 2px 0; + text-decoration: none; + display: flex; + position: relative; +} + +.main-left .nav > li > a:first-child .menu-icon { + width: 20px; + height: 20px; + color: var(--fluent-text-secondary); + flex-shrink: 0; + justify-content: center; + align-items: center; + display: inline-flex; + position: relative; +} + +.main-left .nav > li > a:first-child .menu-icon:before, .main-left .nav > li > a:first-child .menu-icon:after { + content: ""; + top: 0; + width: 100%; + height: 100%; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-mask-image: var(--icon-data); + transition: opacity var(--fluent-duration-normal) var(--fluent-easing-standard); + background-color: currentColor; + position: absolute; + inset-inline-start: 0; +} + +.main-left .nav > li > a:first-child .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01zm5.122-.826a.5.5 0 0 0-.645-.053l-.068.06l-2.616 2.713a.5.5 0 0 0-.057.623l.063.078l2.616 2.615a.5.5 0 0 0 .62.07l.078-.061l2.758-2.627a.5.5 0 0 0 .054-.638l-.059-.069z'/%3E%3C/svg%3E"); + opacity: 1; +} + +.main-left .nav > li > a:first-child .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01z'/%3E%3C/svg%3E"); + opacity: 0; +} + +.main-left .nav > li > a:first-child .menu-label { + white-space: nowrap; + text-overflow: ellipsis; + flex: auto; + min-width: 0; + overflow: hidden; +} + +.main-left .nav > li > a:first-child:hover .menu-icon:before, .main-left .nav > li > a:first-child.active .menu-icon:before { + opacity: 0; +} + +.main-left .nav > li > a:first-child:hover .menu-icon:after, .main-left .nav > li > a:first-child.active .menu-icon:after { + opacity: 1; +} + +.main-left .nav > li > a:first-child:hover { + background: var(--fluent-sidebar-item-hover); + color: var(--fluent-text); +} + +.main-left .nav > li > a:first-child:hover .menu-icon { + color: var(--fluent-text); +} + +.main-left .nav > li > a:first-child:focus-visible { + outline: 2px solid var(--fluent-focus-color); + outline-offset: -2px; +} + +.main-left .nav > li > a:first-child.active { + color: var(--fluent-text); + background: var(--fluent-sidebar-item-active); + font-weight: var(--fluent-font-weight-semibold); +} + +.main-left .nav > li > a:first-child.active .menu-icon { + color: var(--fluent-primary); +} + +.main-left .nav li { + cursor: pointer; + -webkit-user-select: none; + user-select: none; + transition: all var(--fluent-duration-normal) var(--fluent-easing-standard); + padding: 0; + list-style: none; +} + +.main-left .nav li a { + color: var(--fluent-text); + font-weight: var(--fluent-font-weight-regular); + box-shadow: none; + display: block; +} + +.main-left .nav li.slide { + padding: 0; +} + +.main-left .nav li.slide:hover { + background: none; +} + +.main-left .nav li.slide ul { + display: none; + overflow: hidden; +} + +.main-left .nav li.slide .slide-menu { + margin: 0 0 var(--fluent-space-xs) 0; + transition: all var(--fluent-duration-normal) var(--fluent-easing-standard); + background: none; + border-radius: 0; + margin-inline-start: 36px; + padding: 0; + position: relative; +} + +.main-left .nav li.slide .slide-menu .fluent-sidebar-slider { + background-color: var(--fluent-primary); + border-radius: var(--fluent-radius-sm); + width: 3px; + transition: top var(--fluent-duration-normal) var(--fluent-easing-standard),height var(--fluent-duration-normal) var(--fluent-easing-standard); + pointer-events: none; + z-index: 10; + position: absolute; + inset-inline-start: 0; +} + +body[data-tab-animation="0"] .main-left .nav li.slide .slide-menu .fluent-sidebar-slider, body[data-reduce-motion="true"] .main-left .nav li.slide .slide-menu .fluent-sidebar-slider { + transition: none !important; +} + +.main-left .nav li.slide .slide-menu.has-slider li.active:before { + background-color: #0000 !important; +} + +.main-left .nav li.slide .slide-menu.active { + display: block; +} + +.main-left .nav li.slide .slide-menu li { + border-radius: var(--fluent-radius-md); + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); + background: none; + margin: 2px 0; + position: relative; +} + +.main-left .nav li.slide .slide-menu li:before { + content: ""; + inset-inline-start: 0; + border-radius: var(--fluent-radius-sm); + width: 3px; + height: 70%; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + background-color: #0000; + position: absolute; + top: 15%; +} + +.main-left .nav li.slide .slide-menu li:after { + content: none; + display: none; +} + +.main-left .nav li.slide .slide-menu li a { + padding: 0 var(--fluent-space-sm); + border-radius: var(--fluent-radius-md); + align-items: center; + gap: var(--fluent-space-xs); + height: 32px; + box-shadow: none; + transition: color var(--fluent-duration-fast) var(--fluent-easing-standard),background var(--fluent-duration-fast) var(--fluent-easing-standard); + text-decoration: none; + display: flex; +} + +.main-left .nav li.slide .slide-menu li a .menu-icon { + width: 16px; + height: 16px; + color: inherit; + flex-shrink: 0; + justify-content: center; + align-items: center; + display: inline-flex; + position: relative; +} + +.main-left .nav li.slide .slide-menu li a .menu-icon:before, .main-left .nav li.slide .slide-menu li a .menu-icon:after { + content: ""; + top: 0; + width: 100%; + height: 100%; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-image: var(--icon-data); + mask-image: var(--icon-data); + -webkit-mask-image: var(--icon-data); + transition: opacity var(--fluent-duration-normal) var(--fluent-easing-standard); + background-color: currentColor; + position: absolute; + inset-inline-start: 0; +} + +.main-left .nav li.slide .slide-menu li a .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01zm5.122-.826a.5.5 0 0 0-.645-.053l-.068.06l-2.616 2.713a.5.5 0 0 0-.057.623l.063.078l2.616 2.615a.5.5 0 0 0 .62.07l.078-.061l2.758-2.627a.5.5 0 0 0 .054-.638l-.059-.069z'/%3E%3C/svg%3E"); + opacity: 1; +} + +.main-left .nav li.slide .slide-menu li a .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 17a1.5 1.5 0 0 1-1.493-1.355L3 15.501v-11a1.5 1.5 0 0 1 1.356-1.493L4.5 3H9a1.5 1.5 0 0 1 1.493 1.355l.007.145v.254l2.189-2.269a1.5 1.5 0 0 1 2.007-.138l.116.101l2.757 2.725a1.5 1.5 0 0 1 .111 2.011l-.103.116l-2.311 2.2h.234a1.5 1.5 0 0 1 1.493 1.356L17 11v4.5a1.5 1.5 0 0 1-1.355 1.493L15.5 17zm5-6.5H4v5a.5.5 0 0 0 .326.47l.084.023l.09.008h5zm6 0h-5V16h5a.5.5 0 0 0 .492-.41L16 15.5V11a.5.5 0 0 0-.41-.491zm-5-2.79V9.5h1.79zM9 4H4.5a.5.5 0 0 0-.492.411L4 4.501v5h5.5v-5a.5.5 0 0 0-.326-.469L9.09 4.01z'/%3E%3C/svg%3E"); + opacity: 0; +} + +.main-left .nav li.slide .slide-menu li a .menu-label { + white-space: nowrap; + text-overflow: ellipsis; + flex: auto; + min-width: 0; + font-size: 13px; + overflow: hidden; +} + +.main-left .nav li.slide .slide-menu li a[data-title="WireGuard"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="wall" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="clash" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="ssr" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="vpn" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="ipsec" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="tailscale" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="nikki" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="daed" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title$="tunnel" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title$="tier" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title$="proxy" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="WireGuard"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="wall" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="clash" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="ssr" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="vpn" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="ipsec" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="tailscale" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="nikki" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="daed" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title$="tunnel" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title$="tier" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title$="proxy" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.341a3 3 0 0 0-1-.94V5.428a16 16 0 0 1-5.6-2.082l-.4-.249l-.4.249A16 16 0 0 1 4 5.428V9.5c0 3.392 1.968 5.863 6 7.463V17c0 .342.086.663.237.945l-.057.022a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421M12.5 12v-1a2 2 0 1 1 4 0v1h.5a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1zm1-1v1h2v-1a1 1 0 1 0-2 0m1.75 4a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="WireGuard"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="wall" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="clash" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="ssr" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="vpn" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="ipsec" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="tailscale" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="nikki" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="daed" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title$="tunnel" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title$="tier" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title$="proxy" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.5 12v-1a2 2 0 1 1 4 0v1h.5a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1h-5a1 1 0 0 1-1-1v-4a1 1 0 0 1 1-1zm1-1v1h2v-1a1 1 0 1 0-2 0m1.75 4a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.341A3 3 0 0 0 11.5 11v.063A2 2 0 0 0 10 13v4c0 .342.086.663.237.945l-.057.022a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="bypass" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="bypass" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.854 7.146a.5.5 0 1 0-.708.708L9.293 10l-2.147 2.146a.5.5 0 0 0 .708.708L10 10.707l2.146 2.147a.5.5 0 0 0 .708-.708L10.707 10l2.147-2.146a.5.5 0 0 0-.708-.708L10 9.293zm2.423-5.062a.5.5 0 0 0-.554 0a15.05 15.05 0 0 1-6.294 2.421A.5.5 0 0 0 3 5v4.5c0 3.891 2.307 6.73 6.82 8.467a.5.5 0 0 0 .36 0C14.693 16.23 17 13.39 17 9.5V5a.5.5 0 0 0-.43-.495a15.05 15.05 0 0 1-6.293-2.421M4 5.428a16 16 0 0 0 5.6-2.082l.4-.249l.4.249A16 16 0 0 0 16 5.428V9.5c0 3.392-1.968 5.863-6 7.463c-4.032-1.6-6-4.071-6-7.463z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="bypass" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.277 2.084a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.5c0 3.891-2.307 6.73-6.82 8.467a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421a.5.5 0 0 1 .554 0M8.03 6.97a.75.75 0 0 0-1.06 1.06L8.94 10l-1.97 1.97a.75.75 0 1 0 1.06 1.06L10 11.06l1.97 1.97a.75.75 0 1 0 1.06-1.06L11.06 10l1.97-1.97a.75.75 0 0 0-1.06-1.06L10 8.94z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="guard" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="guard" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.6a5.5 5.5 0 0 0-1-.393v-3.78a16 16 0 0 1-5.6-2.081l-.4-.249l-.4.249A16 16 0 0 1 4 5.428V9.5c0 3.242 1.798 5.643 5.478 7.246c.195.435.444.84.74 1.206l-.038.015a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421M19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.146-1.854a.5.5 0 0 0-.708 0L13.5 15.293l-.646-.647a.5.5 0 0 0-.708.708l1 1a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0 0-.708'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="guard" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.6a5.5 5.5 0 0 0-6.782 8.352l-.038.015a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421M19 14.5a4.5 4.5 0 1 0-9 0a4.5 4.5 0 0 0 9 0m-2.146-1.854a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708 0l-1-1a.5.5 0 0 1 .708-.708l.646.647l2.646-2.647a.5.5 0 0 1 .708 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="dns" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="dns" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 18q.74 0 1.442-.13a6 6 0 0 1-.755-1.077c-.233.14-.465.207-.687.207c-.657 0-1.407-.59-2.022-1.908A9.3 9.3 0 0 1 7.42 13.5H10v-1H7.206A15 15 0 0 1 7 10c0-.883.073-1.725.206-2.5h5.588c.096.561.161 1.157.19 1.779a6.3 6.3 0 0 0 .867-.74q.038-.039.078-.074q-.047-.495-.122-.965h2.733c.21.55.354 1.132.419 1.738c.31.224.662.429 1.039.57A8 8 0 1 0 10 18m0-15c.657 0 1.407.59 2.022 1.908c.217.466.406 1.002.559 1.592H7.419c.153-.59.342-1.126.56-1.592C8.592 3.59 9.342 3 10 3M7.072 4.485A10.5 10.5 0 0 0 6.389 6.5H3.936a7.02 7.02 0 0 1 3.778-3.118c-.241.33-.456.704-.642 1.103M6.192 7.5A16 16 0 0 0 6 10c0 .87.067 1.712.193 2.5H3.46A7 7 0 0 1 3 10c0-.88.163-1.724.46-2.5zm.197 6c.176.743.407 1.422.683 2.015c.186.399.401.773.642 1.103A7.02 7.02 0 0 1 3.936 13.5zm5.897-10.118A7.02 7.02 0 0 1 16.064 6.5H13.61a10.5 10.5 0 0 0-.683-2.015a6.6 6.6 0 0 0-.642-1.103m6.215 7.583c-1.37-.193-2.486-1.134-3.066-1.726a.624.624 0 0 0-.87 0c-.58.592-1.695 1.533-3.066 1.726c-.274.038-.499.259-.499.535V14c0 3.468 3.182 4.727 3.87 4.959a.4.4 0 0 0 .26 0C15.818 18.727 19 17.469 19 14v-2.5c0-.276-.225-.497-.499-.535'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="dns" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.987 4.18a12.2 12.2 0 0 0-.669 2.32h5.364a12.2 12.2 0 0 0-.67-2.32c-.301-.733-.648-1.294-1.008-1.663C10.646 2.149 10.307 2 10 2s-.646.149-1.004.517c-.36.37-.707.93-1.009 1.663m-.096-1.899c-.314.426-.59.941-.828 1.518c-.32.78-.58 1.694-.762 2.701H2.804a8.02 8.02 0 0 1 5.087-4.219m4.219 0c.313.426.59.941.827 1.518c.32.78.58 1.694.762 2.701h3.497a8.02 8.02 0 0 0-5.087-4.219M17.602 7.5H13.85q.057.466.094.952a1.626 1.626 0 0 1 2.205.087c.424.433 1.083.983 1.849 1.269a8 8 0 0 0-.396-2.308m-4.76 0q.111.85.145 1.776c-.47.33-1.03.614-1.628.698c-.68.096-1.36.669-1.359 1.526v1H7.158A19 19 0 0 1 7 10c0-.875.056-1.715.158-2.5zm-5.524 6H10v.5c0 1.424.454 2.547 1.074 3.409l-.07.074c-.358.368-.697.517-1.004.517s-.646-.149-1.004-.517c-.36-.37-.707-.93-1.009-1.663a12.2 12.2 0 0 1-.669-2.32m.573 4.219A8.02 8.02 0 0 1 2.804 13.5h3.497c.182 1.007.441 1.922.762 2.7c.237.578.514 1.093.828 1.519M2.398 12.5H6.15A21 21 0 0 1 6 10c0-.866.052-1.705.15-2.5H2.398A8 8 0 0 0 2 10c0 .873.14 1.713.398 2.5m16.103-1.535c-1.37-.193-2.486-1.134-3.066-1.726a.624.624 0 0 0-.87 0c-.58.592-1.695 1.533-3.066 1.726c-.274.038-.499.259-.499.535V14c0 3.468 3.182 4.727 3.87 4.959a.4.4 0 0 0 .26 0C15.818 18.727 19 17.469 19 14v-2.5c0-.276-.225-.497-.499-.535'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="ddns" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="ddns" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 18q.128 0 .254-.004a5.5 5.5 0 0 1-.698-1.083c-.536-.207-1.098-.793-1.578-1.821A9.3 9.3 0 0 1 7.42 13.5h1.672q.096-.52.284-1h-2.17A15 15 0 0 1 7 10c0-.883.073-1.725.206-2.5h5.588c.092.541.156 1.115.186 1.713q.48-.138.992-.188a16 16 0 0 0-.165-1.525h2.733c.251.656.406 1.36.448 2.094q.543.276 1.008.66A8 8 0 1 0 10 18m0-15c.657 0 1.407.59 2.022 1.908c.217.466.406 1.002.559 1.592H7.419c.153-.59.342-1.126.56-1.592C8.592 3.59 9.342 3 10 3M7.072 4.485A10.5 10.5 0 0 0 6.389 6.5H3.936a7.02 7.02 0 0 1 3.778-3.118c-.241.33-.456.704-.642 1.103M6.192 7.5A16 16 0 0 0 6 10c0 .87.067 1.712.193 2.5H3.46A7 7 0 0 1 3 10c0-.88.163-1.724.46-2.5zm.197 6c.176.743.407 1.422.683 2.015c.186.399.401.773.642 1.103A7.02 7.02 0 0 1 3.936 13.5zm5.897-10.118A7.02 7.02 0 0 1 16.064 6.5H13.61a10.5 10.5 0 0 0-.683-2.015a6.6 6.6 0 0 0-.642-1.103M19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4.854-2.353l-2 2a.5.5 0 0 0 .708.707L14 13.707V16.5a.5.5 0 0 0 1 0v-2.793l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.5.5 0 0 0-.351-.146h-.006a.5.5 0 0 0-.348.144z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="ddns" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.987 4.18a12.2 12.2 0 0 0-.669 2.32h5.364a12.2 12.2 0 0 0-.67-2.32c-.301-.733-.648-1.294-1.008-1.663C10.646 2.149 10.307 2 10 2s-.646.149-1.004.517c-.36.37-.707.93-1.009 1.663m-.096-1.899c-.314.426-.59.941-.828 1.518c-.32.78-.58 1.694-.762 2.701H2.804a8.02 8.02 0 0 1 5.087-4.219m4.219 0c.313.426.59.941.827 1.518c.32.78.58 1.694.762 2.701h3.497a8.02 8.02 0 0 0-5.087-4.219M17.602 7.5H13.85q.091.74.128 1.524Q14.236 9 14.5 9c1.328 0 2.546.47 3.496 1.254a8 8 0 0 0-.394-2.754m-4.76 0c.071.546.12 1.119.143 1.711a5.51 5.51 0 0 0-3.61 3.289H7.158A19 19 0 0 1 7 10c0-.875.056-1.715.158-2.5zm-5.524 6h1.773q-.09.487-.091 1c0 1.316.462 2.525 1.234 3.471A1 1 0 0 1 10 18c-.307 0-.646-.149-1.004-.517c-.36-.37-.707-.93-1.009-1.663a12.2 12.2 0 0 1-.669-2.32m.573 4.219A8.02 8.02 0 0 1 2.804 13.5h3.497c.182 1.007.441 1.922.762 2.7c.237.578.514 1.093.828 1.519M2.398 12.5H6.15A21 21 0 0 1 6 10c0-.866.052-1.705.15-2.5H2.398A8 8 0 0 0 2 10c0 .873.14 1.713.398 2.5M19 14.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4.854-2.353l-2 2a.5.5 0 0 0 .708.707L14 13.707V16.5a.5.5 0 0 0 1 0v-2.793l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.5.5 0 0 0-.351-.146h-.006a.5.5 0 0 0-.348.144z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Containers"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title="Software"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="docker" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Containers"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title="Software"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="docker" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M11.3 2.48a3.5 3.5 0 0 0-2.6 0L2.943 4.785A1.5 1.5 0 0 0 2 6.176v7.646a1.5 1.5 0 0 0 .943 1.393L8.7 17.518a3.5 3.5 0 0 0 2.6 0l5.757-2.303A1.5 1.5 0 0 0 18 13.822V6.176a1.5 1.5 0 0 0-.943-1.392zm-2.228.93a2.5 2.5 0 0 1 1.857 0l5.225 2.09l-2.279.91l-6.154-2.46zM6.375 4.487l6.154 2.461L10 7.961L3.846 5.499zm4.125 4.35l6.5-2.6v7.584a.5.5 0 0 1-.314.465l-5.757 2.303q-.21.083-.429.128zm-1 0v7.88a2.5 2.5 0 0 1-.428-.128l-5.758-2.303A.5.5 0 0 1 3 13.822V6.238z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Containers"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title="Software"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="docker" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='m10 7.96l3.029-1.21l-7.5-3l-2.586 1.034a1.5 1.5 0 0 0-.364.208zM2.035 5.853A1.5 1.5 0 0 0 2 6.176v7.646a1.5 1.5 0 0 0 .943 1.393L8.7 17.518q.391.156.8.214V8.838zm8.465 11.88a3.5 3.5 0 0 0 .8-.214l5.757-2.303A1.5 1.5 0 0 0 18 13.822V6.176q0-.166-.035-.324L10.5 8.838zm6.921-12.74l-3.046 1.219l-7.5-3L8.7 2.48a3.5 3.5 0 0 1 2.6 0l5.757 2.303a1.5 1.5 0 0 1 .364.208'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Quick_File_Manager"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="file" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Quick_File_Manager"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="file" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3 5.5v6.604l1.5-2.6a3 3 0 0 1 2.599-1.5H15V7.5A1.5 1.5 0 0 0 13.5 6h-4a.5.5 0 0 1-.353-.146L7.44 4.146A.5.5 0 0 0 7.086 4H4.5A1.5 1.5 0 0 0 3 5.5m1.284 10.485q.106.015.22.015H13.9a2 2 0 0 0 1.732-1l2.163-3.745a1.5 1.5 0 0 0-1.3-2.25H7.1a2 2 0 0 0-1.732 1L3.205 13.75a1.5 1.5 0 0 0 1.079 2.235M2 14.46V5.5A2.5 2.5 0 0 1 4.5 3h2.586a1.5 1.5 0 0 1 1.06.44L9.708 5H13.5A2.5 2.5 0 0 1 16 7.5v.505h.496c1.925 0 3.128 2.083 2.166 3.75L16.499 15.5a3 3 0 0 1-2.598 1.5h-9.4q-.095 0-.186-.007a2.46 2.46 0 0 1-1.438-.592A2.5 2.5 0 0 1 2 14.461'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Quick_File_Manager"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="file" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 3A2.5 2.5 0 0 0 2 5.5v6.971l1.568-2.716A3.5 3.5 0 0 1 6.6 8.005H16V7.5A2.5 2.5 0 0 0 13.5 5H9.707l-1.56-1.56A1.5 1.5 0 0 0 7.085 3zm-.066 7.255A2.5 2.5 0 0 1 6.6 9.005h10.396c1.54 0 2.502 1.666 1.732 3l-2.162 3.745A2.5 2.5 0 0 1 14.4 17H4.004c-1.54 0-2.502-1.667-1.732-3z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Argon_Config"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title="Fluent_Theme"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="theme" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Argon_Config"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title="Fluent_Theme"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="theme" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M5.5 1.999a.5.5 0 0 0-.5.5v8.504a2 2 0 0 0 2 2h1.004v2.998a2 2 0 1 0 4 0v-2.998H13a2 2 0 0 0 2-2V2.5a.5.5 0 0 0-.5-.5zm.5 8.004h8v1a1 1 0 0 1-1 1h-1.496a.5.5 0 0 0-.5.5v3.498a1 1 0 1 1-2 0v-3.498a.5.5 0 0 0-.5-.5H7a1 1 0 0 1-1-1zm8-1H6V3h4v1.5a.5.5 0 0 0 1 0V3h1v2.502a.5.5 0 0 0 1 0V3h1z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Argon_Config"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title="Fluent_Theme"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="theme" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 4.499v-2.5H5.5a.5.5 0 0 0-.5.5v6.5h10V2.5a.5.5 0 0 0-.5-.5H13v3.502a.5.5 0 0 1-1 0V2h-1v2.5a.5.5 0 0 1-1 0m5 5.5H5v1.004a2 2 0 0 0 2 2h1.5v3.498a1.5 1.5 0 1 0 3 0v-3.498H13a2 2 0 0 0 2-2z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Configuration"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title="System"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="config" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="setting" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Configuration"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title="System"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="config" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="setting" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M1.911 7.383a8.5 8.5 0 0 1 1.78-3.08a.5.5 0 0 1 .54-.135l1.918.686a1 1 0 0 0 1.32-.762l.366-2.006a.5.5 0 0 1 .388-.4a8.5 8.5 0 0 1 3.554 0a.5.5 0 0 1 .388.4l.366 2.006a1 1 0 0 0 1.32.762l1.919-.686a.5.5 0 0 1 .54.136a8.5 8.5 0 0 1 1.78 3.079a.5.5 0 0 1-.153.535l-1.555 1.32a1 1 0 0 0 0 1.524l1.555 1.32a.5.5 0 0 1 .152.535a8.5 8.5 0 0 1-1.78 3.08a.5.5 0 0 1-.54.135l-1.918-.686a1 1 0 0 0-1.32.762l-.366 2.007a.5.5 0 0 1-.388.399a8.5 8.5 0 0 1-3.554 0a.5.5 0 0 1-.388-.4l-.366-2.006a1 1 0 0 0-1.32-.762l-1.918.686a.5.5 0 0 1-.54-.136a8.5 8.5 0 0 1-1.78-3.079a.5.5 0 0 1 .152-.535l1.555-1.32a1 1 0 0 0 0-1.524l-1.555-1.32a.5.5 0 0 1-.152-.535m1.06-.006l1.294 1.098a2 2 0 0 1 0 3.05l-1.293 1.098c.292.782.713 1.51 1.244 2.152l1.596-.57q.155-.055.315-.085a2 2 0 0 1 2.326 1.609l.304 1.669a7.6 7.6 0 0 0 2.486 0l.304-1.67a1.998 1.998 0 0 1 2.641-1.524l1.596.571a7.5 7.5 0 0 0 1.245-2.152l-1.294-1.098a1.998 1.998 0 0 1 0-3.05l1.294-1.098a7.5 7.5 0 0 0-1.245-2.152l-1.596.57a2 2 0 0 1-2.64-1.524l-.305-1.669a7.6 7.6 0 0 0-2.486 0l-.304 1.669a2 2 0 0 1-2.64 1.525l-1.597-.571a7.5 7.5 0 0 0-1.244 2.152M7.502 10a2.5 2.5 0 1 1 5 0a2.5 2.5 0 0 1-5 0m1 0a1.5 1.5 0 1 0 3 0a1.5 1.5 0 0 0-3 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Configuration"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title="System"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="config" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="setting" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M1.911 7.383a8.5 8.5 0 0 1 1.78-3.08a.5.5 0 0 1 .54-.135l1.918.686a1 1 0 0 0 1.32-.762l.366-2.006a.5.5 0 0 1 .388-.4a8.5 8.5 0 0 1 3.554 0a.5.5 0 0 1 .388.4l.366 2.006a1 1 0 0 0 1.32.762l1.919-.686a.5.5 0 0 1 .54.136a8.5 8.5 0 0 1 1.78 3.079a.5.5 0 0 1-.153.535l-1.555 1.32a1 1 0 0 0 0 1.524l1.555 1.32a.5.5 0 0 1 .152.535a8.5 8.5 0 0 1-1.78 3.08a.5.5 0 0 1-.54.135l-1.918-.686a1 1 0 0 0-1.32.762l-.366 2.007a.5.5 0 0 1-.388.399a8.5 8.5 0 0 1-3.554 0a.5.5 0 0 1-.388-.4l-.366-2.006a1 1 0 0 0-1.32-.762l-1.918.686a.5.5 0 0 1-.54-.136a8.5 8.5 0 0 1-1.78-3.079a.5.5 0 0 1 .152-.535l1.555-1.32a1 1 0 0 0 0-1.524l-1.555-1.32a.5.5 0 0 1-.152-.535M8.001 10a2 2 0 1 0 4 0a2 2 0 0 0-4 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Reboot"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="reboot" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Reboot"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="reboot" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.5 2.5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0zM13.743 4a.5.5 0 1 0-.499.867a6.5 6.5 0 1 1-6.494.004a.5.5 0 1 0-.5-.866A7.5 7.5 0 1 0 13.743 4'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Reboot"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="reboot" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.75 2.5a.75.75 0 0 0-1.5 0v6a.75.75 0 0 0 1.5 0zM13.743 4a.75.75 0 1 0-.748 1.3A6 6 0 1 1 7 5.305a.75.75 0 1 0-.75-1.3a7.5 7.5 0 1 0 7.493-.003'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Events"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title="System_Log"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="log" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Events"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title="System_Log"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="log" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M6 2a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h4.257a5.5 5.5 0 0 1-.657-1H6a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h4v3.5A1.5 1.5 0 0 0 11.5 8H15v1.022q.516.047 1 .185V7.414a1.5 1.5 0 0 0-.44-1.06l-3.914-3.915A1.5 1.5 0 0 0 10.586 2zm8.793 5H11.5a.5.5 0 0 1-.5-.5V3.207zM9 14.5q0-.252.022-.5H8.5a.5.5 0 0 0 0 1h.522A6 6 0 0 1 9 14.5m.207-1.5q.149-.524.393-1H8.5a.5.5 0 0 0 0 1zm1.05-2c.313-.38.677-.716 1.08-1H8.5a.5.5 0 0 0 0 1zM6.5 10a.5.5 0 1 0 0 1a.5.5 0 0 0 0-1M6 12.5a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0m0 2a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0m13 0a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4-3a.5.5 0 0 0-1 0v3a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 0-1H15z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Events"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title="System_Log"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="log" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cg fill='currentColor'%3E%3Cpath d='M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v13A1.5 1.5 0 0 0 5.5 18h4.757a5.5 5.5 0 0 1-1.235-3H8.5a.5.5 0 0 1 0-1h.522a5.5 5.5 0 0 1 .185-1H8.5a.5.5 0 0 1 0-1h1.1q.276-.538.657-1H8.5a.5.5 0 0 1 0-1h2.837c.895-.63 1.986-1 3.163-1a5.5 5.5 0 0 1 1.5.207V8h-4.5A1.5 1.5 0 0 1 10 6.5m-4 4a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0m0 2a.5.5 0 1 1 1 0a.5.5 0 0 1-1 0m.5 1.5a.5.5 0 1 1 0 1a.5.5 0 0 1 0-1'/%3E%3Cpath d='M11 6.5V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5m8 8a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4-3a.5.5 0 0 0-1 0v3a.5.5 0 0 0 .5.5h2a.5.5 0 0 0 0-1H15z'/%3E%3C/g%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Traffic_Monitor"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="monitor" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="statistics" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="bandix" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="Graphs" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Traffic_Monitor"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="monitor" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="statistics" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="bandix" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="Graphs" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 10a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3a.5.5 0 0 0-.5-.5M6 8.5a.5.5 0 0 1 1 0v5a.5.5 0 0 1-1 0zM13.5 6a.5.5 0 0 0-.5.5v7a.5.5 0 0 0 1 0v-7a.5.5 0 0 0-.5-.5M3 5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zm1 0v10a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Traffic_Monitor"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="monitor" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="statistics" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="bandix" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="Graphs" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M5 3a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm5 7a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-1 0v-3a.5.5 0 0 1 .5-.5M6 8.5a.5.5 0 0 1 1 0v5a.5.5 0 0 1-1 0zM13.5 6a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="S.M.A.R.T." i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="info" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="S.M.A.R.T." i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="info" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.492 8.91A.5.5 0 0 0 9.5 9v4.502l.008.09a.5.5 0 0 0 .992-.09V9zm.307-2.16a.75.75 0 1 0-1.5 0a.75.75 0 0 0 1.5 0M18 10a8 8 0 1 0-16 0a8 8 0 0 0 16 0M3 10a7 7 0 1 1 14 0a7 7 0 0 1-14 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="S.M.A.R.T." i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="info" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M18 10a8 8 0 1 0-16 0a8 8 0 0 0 16 0M9.508 8.91a.5.5 0 0 1 .984 0L10.5 9v4.502l-.008.09a.5.5 0 0 1-.984 0l-.008-.09V9zM9.25 6.75a.75.75 0 1 1 1.5 0a.75.75 0 0 1-1.5 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="Setup" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="Setup" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M15 11q.221 0 .435.023c.2.022.355.178.397.373l.222 1.036a.5.5 0 0 0 .642.37l1.006-.325a.46.46 0 0 1 .52.156q.26.352.439.758a.465.465 0 0 1-.125.529l-.785.709a.5.5 0 0 0 0 .742l.785.708a.465.465 0 0 1 .125.53q-.178.405-.439.757a.465.465 0 0 1-.52.157l-1.006-.326a.5.5 0 0 0-.642.371l-.222 1.034a.47.47 0 0 1-.397.374a4 4 0 0 1-.873 0a.465.465 0 0 1-.396-.373l-.22-1.035a.5.5 0 0 0-.643-.37l-1.006.325a.46.46 0 0 1-.52-.157a4 4 0 0 1-.44-.758a.46.46 0 0 1 .126-.529l.785-.708a.5.5 0 0 0 0-.742l-.785-.71a.46.46 0 0 1-.125-.528q.179-.406.438-.758a.46.46 0 0 1 .52-.156l1.007.326a.5.5 0 0 0 .642-.371l.22-1.036a.465.465 0 0 1 .397-.373Q14.778 11 15 11m0 3a1 1 0 1 0 0 2a1 1 0 0 0 0-2M7.499 1q.455 0 .892.063l.288.045l.073.02a.5.5 0 0 1 .326.368l.395 1.844a.5.5 0 0 0 .642.37l1.792-.58l.073-.018a.5.5 0 0 1 .461.168c.51.598.915 1.29 1.184 2.046a.5.5 0 0 1-.137.538l-1.4 1.265a.5.5 0 0 0 0 .742l1.4 1.264a.5.5 0 0 1 .137.538a6.5 6.5 0 0 1-1.184 2.046a.5.5 0 0 1-.534.15l-1.792-.58a.5.5 0 0 0-.642.371l-.395 1.843a.5.5 0 0 1-.4.388q-.57.107-1.18.109c-.404 0-.8-.04-1.18-.11a.5.5 0 0 1-.4-.387l-.393-1.843a.5.5 0 0 0-.642-.37l-1.793.58a.5.5 0 0 1-.534-.151a6.5 6.5 0 0 1-1.184-2.046a.5.5 0 0 1 .137-.538l1.4-1.264a.5.5 0 0 0 .072-.661l-.072-.081l-1.4-1.265a.5.5 0 0 1-.137-.538A6.5 6.5 0 0 1 2.556 3.28l.052-.052a.5.5 0 0 1 .482-.098l1.793.58a.5.5 0 0 0 .642-.37l.394-1.844l.021-.072a.5.5 0 0 1 .377-.316Q6.891 1.001 7.5 1m0 1q-.342 0-.674.043l-.321 1.506a1.5 1.5 0 0 1-1.93 1.113L3.11 4.188q-.407.537-.676 1.166L3.58 6.387l.116.116a1.5 1.5 0 0 1-.116 2.11L2.434 9.645c.178.417.406.808.676 1.165l1.464-.472a1.5 1.5 0 0 1 1.93 1.113l.321 1.504q.333.043.674.045q.34-.002.673-.045l.322-1.504a1.5 1.5 0 0 1 1.93-1.113l1.462.473c.27-.358.498-.749.675-1.166L11.42 8.613a1.5 1.5 0 0 1 0-2.226l1.142-1.033a5.5 5.5 0 0 0-.675-1.166l-1.462.474a1.5 1.5 0 0 1-1.93-1.113l-.322-1.506A6 6 0 0 0 7.499 2m0 3.25a2.25 2.25 0 1 1 0 4.5a2.25 2.25 0 0 1 0-4.5m0 1a1.25 1.25 0 1 0 .002 2.501A1.25 1.25 0 0 0 7.5 6.25'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="Setup" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M15.003 11q.221 0 .436.023c.2.022.355.178.397.373l.222 1.036a.5.5 0 0 0 .642.37l1.005-.325a.47.47 0 0 1 .522.156q.26.352.438.757a.465.465 0 0 1-.125.529l-.786.71a.5.5 0 0 0 0 .742l.786.71a.465.465 0 0 1 .125.528q-.18.405-.438.757a.47.47 0 0 1-.522.157l-1.005-.326a.5.5 0 0 0-.642.371l-.222 1.034a.47.47 0 0 1-.396.374a4 4 0 0 1-.874 0a.465.465 0 0 1-.396-.373l-.22-1.035a.5.5 0 0 0-.644-.37l-1.006.325a.46.46 0 0 1-.52-.157a4 4 0 0 1-.438-.758a.465.465 0 0 1 .125-.529l.785-.708a.5.5 0 0 0 0-.742l-.785-.71a.465.465 0 0 1-.125-.528q.178-.406.437-.758a.46.46 0 0 1 .52-.156l1.007.326a.5.5 0 0 0 .643-.371l.22-1.036a.465.465 0 0 1 .397-.373q.216-.023.437-.023m0 3a1 1 0 1 0 0 2a1 1 0 0 0 0-2M7.5 1q.455 0 .892.063l.288.045l.073.02a.5.5 0 0 1 .326.368l.395 1.844a.5.5 0 0 0 .642.37l1.792-.58l.073-.018a.5.5 0 0 1 .461.168a6.5 6.5 0 0 1 1.184 2.046a.5.5 0 0 1-.137.538l-1.4 1.265a.5.5 0 0 0 0 .742l1.4 1.264a.5.5 0 0 1 .137.538a6.5 6.5 0 0 1-1.184 2.046a.5.5 0 0 1-.534.15l-1.792-.58a.5.5 0 0 0-.642.371l-.395 1.843a.5.5 0 0 1-.4.388q-.57.107-1.179.109c-.405 0-.8-.04-1.182-.11a.5.5 0 0 1-.398-.387l-.394-1.843a.5.5 0 0 0-.642-.37l-1.793.58a.5.5 0 0 1-.534-.151a6.5 6.5 0 0 1-1.184-2.046a.5.5 0 0 1 .137-.538l1.4-1.264a.5.5 0 0 0 .072-.661l-.072-.081l-1.4-1.265a.5.5 0 0 1-.137-.538A6.5 6.5 0 0 1 2.556 3.28l.052-.052a.5.5 0 0 1 .482-.098l1.793.58a.5.5 0 0 0 .642-.37l.394-1.844l.021-.072a.5.5 0 0 1 .377-.316Q6.891 1.001 7.5 1m0 4.75a1.75 1.75 0 1 0 0 3.5a1.75 1.75 0 0 0 0-3.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="CPU_Freq"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="cpu" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="CPU_Freq"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="cpu" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 7a3 3 0 1 0 0 6a3 3 0 0 0 0-6m-2 3a2 2 0 1 1 4 0a2 2 0 0 1-4 0m-.5-8a.5.5 0 0 1 .5.5V4h1.5V2.5a.5.5 0 0 1 1 0V4H12V2.5a.5.5 0 0 1 1 0V4h.5A2.5 2.5 0 0 1 16 6.5V7h1.5a.5.5 0 0 1 0 1H16v1.5h1.5a.5.5 0 0 1 0 1H16V12h1.5a.5.5 0 0 1 0 1H16v.5a2.5 2.5 0 0 1-2.5 2.5H13v1.5a.5.5 0 0 1-1 0V16h-1.5v1.5a.5.5 0 0 1-1 0V16H8v1.5a.5.5 0 0 1-1 0V16h-.5A2.5 2.5 0 0 1 4 13.5V13H2.5a.5.5 0 0 1 0-1H4v-1.5H2.5a.5.5 0 0 1 0-1H4V8H2.5a.5.5 0 0 1 0-1H4v-.5A2.5 2.5 0 0 1 6.5 4H7V2.5a.5.5 0 0 1 .5-.5M15 6.5A1.5 1.5 0 0 0 13.5 5h-7A1.5 1.5 0 0 0 5 6.5v7A1.5 1.5 0 0 0 6.5 15h7a1.5 1.5 0 0 0 1.5-1.5z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="CPU_Freq"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="cpu" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 12a2 2 0 1 0 0-4a2 2 0 0 0 0 4M8 2.5a.5.5 0 0 0-1 0V4h-.5A2.5 2.5 0 0 0 4 6.5V7H2.5a.5.5 0 0 0 0 1H4v1.5H2.5a.5.5 0 0 0 0 1H4V12H2.5a.5.5 0 0 0 0 1H4v.5A2.5 2.5 0 0 0 6.5 16H7v1.5a.5.5 0 0 0 1 0V16h1.5v1.5a.5.5 0 0 0 1 0V16H12v1.5a.5.5 0 0 0 1 0V16h.5a2.5 2.5 0 0 0 2.5-2.5V13h1.5a.5.5 0 0 0 0-1H16v-1.5h1.5a.5.5 0 0 0 0-1H16V8h1.5a.5.5 0 0 0 0-1H16v-.5A2.5 2.5 0 0 0 13.5 4H13V2.5a.5.5 0 0 0-1 0V4h-1.5V2.5a.5.5 0 0 0-1 0V4H8zm5 7.5a3 3 0 1 1-6 0a3 3 0 0 1 6 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="LED_Configuration"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="led" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="LED_Configuration"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="led" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 2c3.314 0 6 2.597 6 5.8c0 1.677-.745 3.216-2.204 4.594a.6.6 0 0 0-.145.213l-.026.081l-.936 3.917c-.184.771-.865 1.33-1.67 1.39l-.144.005h-1.75c-.818 0-1.535-.516-1.776-1.262l-.038-.133l-.935-3.916a.6.6 0 0 0-.17-.295c-1.39-1.312-2.133-2.77-2.2-4.355L4 7.8l.003-.191C4.108 4.494 6.753 2 10 2m2.045 13h-4.09l.319 1.334l.026.096c.097.3.376.522.712.563l.113.007h1.713l.106-.003a.86.86 0 0 0 .741-.525l.031-.1zM10 3C7.368 3 5.212 4.953 5.015 7.414l-.012.212L5 7.779l.004.217c.056 1.304.674 2.525 1.888 3.671c.188.178.33.398.414.64l.043.15L7.716 14h4.569l.387-1.615l.051-.15c.086-.215.218-.409.386-.568C14.383 10.465 15 9.181 15 7.8C15 5.157 12.769 3 10 3'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="LED_Configuration"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="led" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='m13.073 15l-.384 1.605c-.184.771-.865 1.33-1.67 1.39l-.144.005h-1.75c-.818 0-1.535-.516-1.776-1.262l-.038-.133L6.928 15zM10 2c3.314 0 6 2.597 6 5.8c0 1.677-.745 3.216-2.204 4.594a.6.6 0 0 0-.145.213l-.026.081L13.311 14H6.689l-.313-1.311a.6.6 0 0 0-.17-.295c-1.39-1.312-2.133-2.77-2.2-4.355L4 7.8l.003-.191C4.108 4.494 6.753 2 10 2'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Administration"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="kms" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Administration"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="kms" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M15 6a1 1 0 1 1-2 0a1 1 0 0 1 2 0m-2.5-4C9.424 2 7 4.424 7 7.5c0 .397.04.796.122 1.175c.058.27-.008.504-.142.638l-4.54 4.54A1.5 1.5 0 0 0 2 14.915V16.5A1.5 1.5 0 0 0 3.5 18h2A1.5 1.5 0 0 0 7 16.5V16h1a1 1 0 0 0 1-1v-1h1a1 1 0 0 0 1-1v-.18c.493.134 1.007.18 1.5.18c3.076 0 5.5-2.424 5.5-5.5S15.576 2 12.5 2M8 7.5C8 4.976 9.976 3 12.5 3S17 4.976 17 7.5S15.024 12 12.5 12c-.66 0-1.273-.095-1.776-.347A.5.5 0 0 0 10 12.1v.9H9a1 1 0 0 0-1 1v1H7a1 1 0 0 0-1 1v.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-1.586a.5.5 0 0 1 .146-.353l4.541-4.541c.432-.432.522-1.044.412-1.556A4.6 4.6 0 0 1 8 7.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Administration"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="kms" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7 7.5C7 4.424 9.424 2 12.5 2S18 4.424 18 7.5S15.576 13 12.5 13a5.8 5.8 0 0 1-1.5-.18V13a1 1 0 0 1-1 1H9v1a1 1 0 0 1-1 1H7v.5A1.5 1.5 0 0 1 5.5 18h-2A1.5 1.5 0 0 1 2 16.5v-1.586c0-.398.158-.78.44-1.06l4.54-4.541c.134-.134.2-.368.142-.638A5.6 5.6 0 0 1 7 7.5M15 6a1 1 0 1 0-2 0a1 1 0 0 0 2 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="DHCP"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="upnp" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="DHCP"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="upnp" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.5 5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM7 12.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m.5 1.5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zM5 4.5A2.5 2.5 0 0 1 7.5 2h5A2.5 2.5 0 0 1 15 4.5v11a2.5 2.5 0 0 1-2.5 2.5h-5A2.5 2.5 0 0 1 5 15.5zM7.5 3A1.5 1.5 0 0 0 6 4.5v11A1.5 1.5 0 0 0 7.5 17h5a1.5 1.5 0 0 0 1.5-1.5v-11A1.5 1.5 0 0 0 12.5 3z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="DHCP"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="upnp" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.5 2A2.5 2.5 0 0 0 5 4.5v11A2.5 2.5 0 0 0 7.5 18h5a2.5 2.5 0 0 0 2.5-2.5v-11A2.5 2.5 0 0 0 12.5 2zM7 5.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 7a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5m0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Interfaces"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="network" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Interfaces"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="network" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath fill='currentColor' d='M4 6.5v3a.5.5 0 1 1-1 0v-3a.5.5 0 1 1 1 0M6.5 6a.5.5 0 0 0-.5.5v3a.5.5 0 1 0 1 0v-3a.5.5 0 0 0-.5-.5m3 0a.5.5 0 0 0-.5.5v3a.5.5 0 1 0 1 0v-3a.5.5 0 0 0-.5-.5m3 0a.5.5 0 0 0-.5.5v3a.5.5 0 0 0 1 0v-3a.5.5 0 0 0-.5-.5m2.5-.5v5a1.5 1.5 0 0 1-1.5 1.5H13v1.5a.5.5 0 0 1-.5.5h-6a.5.5 0 0 1-.5-.5V12H2v1.5a.5.5 0 0 1-1 0V3H.5a.5.5 0 0 1 0-1H1a1 1 0 0 1 1 1v1h11.5A1.5 1.5 0 0 1 15 5.5M7 13h2v-1H7zm5-1h-2v1h2zm2-6.5a.5.5 0 0 0-.5-.5H2v6h11.5a.5.5 0 0 0 .5-.5z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Interfaces"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="network" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' fill='none'%3E%3Cpath fill='currentColor' d='M14.561 4.439A1.5 1.5 0 0 0 13.5 4H2V3a.997.997 0 0 0-1-1H.5a.5.5 0 1 0 0 1H1v10.5a.5.5 0 1 0 1 0V12h4v1.5a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V12h.5a1.503 1.503 0 0 0 1.5-1.5v-5c0-.398-.158-.779-.439-1.061M4 9.5a.5.5 0 0 1-.5.5a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0zm2.5.5a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v3a.5.5 0 0 1-.5.5M9 13H7v-1h2zm.5-3a.5.5 0 0 1-.5-.5v-3a.5.5 0 1 1 1 0v3a.5.5 0 0 1-.5.5m2.5 3h-2v-1h2zm1-3.5a.5.5 0 1 1-1 0v-3a.5.5 0 0 1 .5-.5a.5.5 0 0 1 .5.5z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Wireless"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="wireless" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Wireless"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="wireless" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M16.832 7.384c.41.409.796.877 1.133 1.365a.5.5 0 0 1-.823.568A8.612 8.612 0 0 0 3.946 8.091c-.348.348-.693.77-1.007 1.222a.5.5 0 0 1-.821-.57a10 10 0 0 1 1.121-1.36a9.61 9.61 0 0 1 13.593 0m-2.236 1.973a6.6 6.6 0 0 1 1.194 1.656a.5.5 0 0 1-.89.455a5.6 5.6 0 0 0-1.01-1.404a5.45 5.45 0 0 0-8.716 1.39a.5.5 0 1 1-.892-.452a6.45 6.45 0 0 1 10.315-1.645m-1.709 2.497c.355.354.647.79.85 1.254a.5.5 0 0 1-.916.401a3.1 3.1 0 0 0-.64-.948a3.033 3.033 0 0 0-4.29 0a3 3 0 0 0-.631.938a.5.5 0 1 1-.916-.401a4 4 0 0 1 .84-1.244a4.033 4.033 0 0 1 5.703 0m-1.924 1.933a1.298 1.298 0 1 1-1.836 1.836a1.298 1.298 0 0 1 1.836-1.836'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Wireless"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="wireless" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M16.73 7.563c.403.404.783.864 1.115 1.344a.75.75 0 0 1-1.235.852A7.968 7.968 0 0 0 4.4 8.623c-.32.322-.64.712-.932 1.133a.75.75 0 1 1-1.233-.855c.34-.49.716-.95 1.105-1.338a9.47 9.47 0 0 1 13.39 0m-2.14 1.888c.473.473.88 1.045 1.192 1.653a.75.75 0 1 1-1.335.683a5.1 5.1 0 0 0-.918-1.275a4.943 4.943 0 0 0-7.904 1.26a.75.75 0 0 1-1.338-.677A6.443 6.443 0 0 1 14.59 9.451m-1.635 2.39c.363.363.662.808.87 1.282a.75.75 0 0 1-1.375.602a2.7 2.7 0 0 0-.556-.824a2.63 2.63 0 0 0-3.72 0a2.7 2.7 0 0 0-.547.814a.75.75 0 1 1-1.374-.601a4.2 4.2 0 0 1 .86-1.274a4.13 4.13 0 0 1 5.842 0m-2.034 2.04a1.242 1.242 0 1 1-1.757 1.758a1.242 1.242 0 0 1 1.757-1.757'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="address" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="address" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.5 4A2.5 2.5 0 0 0 2 6.5v7A2.5 2.5 0 0 0 4.5 16h11a2.5 2.5 0 0 0 2.5-2.5v-7A2.5 2.5 0 0 0 15.5 4zM3 6.5A1.5 1.5 0 0 1 4.5 5h11A1.5 1.5 0 0 1 17 6.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 3 13.5zM4.5 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h8a.5.5 0 0 0 0-1zm.5 3a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="address" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2 6.5A2.5 2.5 0 0 1 4.5 4h11A2.5 2.5 0 0 1 18 6.5v7a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 13.5zM9.5 6h-5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1m3 2h-8a.5.5 0 0 0 0 1h8a.5.5 0 0 0 0-1M5 11a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1a1 1 0 0 0-1-1z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="qos" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="qos" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2 4a2 2 0 0 1 2-2h12a2 2 0 1 1 0 4H4a2 2 0 0 1-2-2m2-1a1 1 0 0 0 0 2h12a1 1 0 1 0 0-2zm0 7a2 2 0 0 1 2-2h8a2 2 0 1 1 0 4H6a2 2 0 0 1-2-2m2-1a1 1 0 0 0 0 2h8a1 1 0 1 0 0-2zm2 5a2 2 0 1 0 0 4h4a2 2 0 1 0 0-4zm-1 2a1 1 0 0 1 1-1h4a1 1 0 1 1 0 2H8a1 1 0 0 1-1-1'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="qos" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2 4a2 2 0 0 1 2-2h12a2 2 0 1 1 0 4H4a2 2 0 0 1-2-2m2 6a2 2 0 0 1 2-2h8a2 2 0 1 1 0 4H6a2 2 0 0 1-2-2m4 4a2 2 0 1 0 0 4h4a2 2 0 1 0 0-4z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="PortWeaver" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="socat" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="PortWeaver" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="socat" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.146 3.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-3 3a.5.5 0 0 1-.708-.708L14.293 7H4.5a.5.5 0 0 1 0-1h9.793l-2.147-2.146a.5.5 0 0 1 0-.708m-4.292 7a.5.5 0 0 1 0 .708L5.707 13H15.5a.5.5 0 0 1 0 1H5.707l2.147 2.146a.5.5 0 0 1-.708.708l-3-3a.5.5 0 0 1 0-.708l3-3a.5.5 0 0 1 .708 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="PortWeaver" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="socat" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.78 3.22a.75.75 0 1 0-1.06 1.06L13.44 6H4.75a.75.75 0 0 0 0 1.5h8.69l-1.72 1.72a.75.75 0 1 0 1.06 1.06l3-3a.75.75 0 0 0 0-1.06zm-4.5 7.56a.75.75 0 1 0-1.06-1.06l-3 3a.75.75 0 0 0 0 1.06l3 3a.75.75 0 0 0 1.06-1.06L6.56 14h8.69a.75.75 0 0 0 0-1.5H6.56z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="usteer" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="roam" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="mesh" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="usteer" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="roam" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="mesh" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M5.879 6.586a2.5 2.5 0 1 1 .707-.707L8.62 7.914A2.5 2.5 0 0 1 9.999 7.5c.51 0 .983.152 1.379.414l2.035-2.035a2.5 2.5 0 1 1 .707.707L12.086 8.62c.262.396.414.87.414 1.379c0 .51-.152.983-.414 1.379l2.035 2.035a2.5 2.5 0 1 1-.707.707l-2.035-2.035a2.5 2.5 0 0 1-1.379.414c-.51 0-.983-.152-1.379-.414L6.586 14.12a2.5 2.5 0 1 1-.707-.707l2.035-2.035A2.5 2.5 0 0 1 7.5 10c0-.51.152-.983.414-1.379zM6 4.5a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0m11 0a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0M4.5 17a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3M17 15.5a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0M11.5 10a1.5 1.5 0 1 0-3 0a1.5 1.5 0 0 0 3 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="usteer" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="roam" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="mesh" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7 4.5c0 .51-.152.983-.414 1.379L8.62 7.914A2.5 2.5 0 0 1 9.999 7.5c.51 0 .983.152 1.379.414l2.035-2.035a2.5 2.5 0 1 1 .707.707L12.086 8.62c.262.396.414.87.414 1.379c0 .51-.152.983-.414 1.379l2.035 2.035a2.5 2.5 0 1 1-.707.707l-2.035-2.035a2.5 2.5 0 0 1-1.379.414c-.51 0-.983-.152-1.379-.414L6.586 14.12a2.5 2.5 0 1 1-.707-.707l2.035-2.035A2.5 2.5 0 0 1 7.5 10c0-.51.152-.983.414-1.379L5.88 6.586A2.5 2.5 0 1 1 7 4.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="share" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="share" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12 5.502a2.502 2.502 0 1 1 .531 1.541L7.915 9.351a2.5 2.5 0 0 1 0 1.297l4.618 2.31a2.5 2.5 0 1 1-.448.894l-4.618-2.31a2.5 2.5 0 1 1 0-3.086l4.618-2.307A2.5 2.5 0 0 1 12 5.502M14.502 4a1.501 1.501 0 1 0 0 3.003a1.501 1.501 0 0 0 0-3.003m-.002 9a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M4 10a1.5 1.5 0 1 0 3 0a1.5 1.5 0 0 0-3 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="share" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M17 5.5a2.5 2.5 0 0 1-4.467 1.543L7.915 9.352a2.5 2.5 0 0 1 0 1.297l4.618 2.308a2.5 2.5 0 1 1-.448.894l-4.618-2.308a2.5 2.5 0 1 1 0-3.085l4.618-2.31A2.504 2.504 0 0 1 14.5 3A2.5 2.5 0 0 1 17 5.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="usb" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="usb" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.5 2h5a.5.5 0 0 1 .5.5V6h.079C14.137 6 15 6.853 15 7.912v5.176A1.916 1.916 0 0 1 13.079 15H12v2.5a.5.5 0 0 1-1 0V15H9v2.5a.5.5 0 0 1-1 0V15H6.921A1.916 1.916 0 0 1 5 13.088V7.912C5 6.852 5.863 6 6.921 6H7V2.5a.5.5 0 0 1 .5-.5m5.579 12a.916.916 0 0 0 .921-.912V7.912c0-.5-.41-.912-.921-.912H6.92a.916.916 0 0 0-.92.912v5.176c0 .5.41.912.921.912zM12 6V3H8v3z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="usb" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7 2.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5V6h.079C14.137 6 15 6.853 15 7.912v5.176A1.916 1.916 0 0 1 13.079 15H12v2.5a.5.5 0 0 1-1 0V15H9v2.5a.5.5 0 0 1-1 0V15H6.921A1.916 1.916 0 0 1 5 13.088V7.912C5 6.852 5.863 6 6.921 6H7zM8 6h4V3H8z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="raid" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="raid" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4 5c0-1.007.875-1.755 1.904-2.223C6.978 2.289 8.427 2 10 2s3.022.289 4.096.777C15.125 3.245 16 3.993 16 5v6h-1V6.698a5 5 0 0 1-.904.525C13.022 7.711 11.573 8 10 8s-3.022-.289-4.096-.777A5 5 0 0 1 5 6.698V15c0 .374.356.875 1.318 1.313c.916.416 2.218.687 3.682.687h.05c.434.425.978.738 1.585.892c-.521.07-1.07.108-1.635.108c-1.573 0-3.022-.289-4.096-.777C4.875 16.755 4 16.007 4 15zm1 0c0 .374.356.875 1.318 1.313C7.234 6.729 8.536 7 10 7s2.766-.27 3.682-.687C14.644 5.875 15 5.373 15 5c0-.374-.356-.875-1.318-1.313C12.766 3.271 11.464 3 10 3s-2.766.27-3.682.687C5.356 4.125 5 4.627 5 5m7.5 7a2.5 2.5 0 0 0 0 5h.5a.5.5 0 0 0 0-1h-.5a1.5 1.5 0 0 1 0-3h.5a.5.5 0 0 0 0-1zm3.5 0a.5.5 0 0 0 0 1h.5a1.5 1.5 0 0 1 0 3H16a.5.5 0 0 0 0 1h.5a2.5 2.5 0 0 0 0-5zm-4 2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="raid" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 8c3.314 0 6-1.343 6-3s-2.686-3-6-3s-6 1.343-6 3s2.686 3 6 3m4.69.016c.47-.235.927-.534 1.31-.896V11h-3.5a3.5 3.5 0 0 0-.878 6.889c-.516.072-1.06.111-1.622.111c-3.314 0-6-1.343-6-3V7.12c.383.362.84.661 1.31.896C6.562 8.642 8.222 9 10 9s3.438-.358 4.69-.984M12.5 12a2.5 2.5 0 0 0 0 5h.5a.5.5 0 0 0 0-1h-.5a1.5 1.5 0 0 1 0-3h.5a.5.5 0 0 0 0-1zm3.5 0a.5.5 0 0 0 0 1h.5a1.5 1.5 0 0 1 0 3H16a.5.5 0 0 0 0 1h.5a2.5 2.5 0 0 0 0-5zm-4 2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="filter" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="filter" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.5 13h5a.5.5 0 0 1 .09.992L12.5 14h-5a.5.5 0 0 1-.09-.992zh5zm-2-4h9a.5.5 0 0 1 .09.992L14.5 10h-9a.5.5 0 0 1-.09-.992zh9zm-2-4h13a.5.5 0 0 1 .09.992L16.5 6h-13a.5.5 0 0 1-.09-.992zh13z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="filter" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M12.25 13.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5zm2-4.25a.75.75 0 0 1 0 1.5h-8.5a.75.75 0 0 1 0-1.5zm2-4.25a.75.75 0 0 1 0 1.5H3.75a.75.75 0 0 1 0-1.5z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="speed" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="homebox" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="speed" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="homebox" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 2a8 8 0 0 1 5.44 13.866l-.082.062a.5.5 0 0 1-.6-.794l.254-.246A6.97 6.97 0 0 0 16.98 10.5h-1.503A.49.49 0 0 1 15 10c0-.268.208-.5.477-.5h1.503a7 7 0 0 0-6.479-6.48v1.502A.49.49 0 0 1 10 5a.49.49 0 0 1-.499-.476V3.02a6.97 6.97 0 0 0-4.083 1.69l1.063 1.063a.49.49 0 0 1-.016.69a.49.49 0 0 1-.69.017L4.71 5.417A6.97 6.97 0 0 0 3.02 9.5h1.503A.49.49 0 0 1 5 10a.49.49 0 0 1-.477.5H3.02a6.97 6.97 0 0 0 1.968 4.388l.253.246l.067.075a.5.5 0 0 1-.747.657A8 8 0 0 1 10 2m3.196 4.102a.502.502 0 0 1 .74.64v.002l-.001.002l-.005.009l-.02.037l-.078.138a195 195 0 0 1-1.15 2.026a80 80 0 0 1-.947 1.61c-.141.232-.271.44-.38.603a3 3 0 0 1-.295.391a1.5 1.5 0 0 1-2.12-2.12c.123-.124.464-.4.865-.718c.415-.33.93-.73 1.432-1.118a347 347 0 0 1 1.795-1.376l.121-.093l.032-.025l.01-.006z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="speed" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="homebox" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 2a8 8 0 0 1 5.518 13.793a.75.75 0 0 1-1.036-1.086A6.5 6.5 0 0 0 16.422 11h-1.174a.75.75 0 0 1 0-1.5h1.232a6.5 6.5 0 0 0-5.729-5.956v1.265c0 .394-.356.691-.751.691c-.394 0-.749-.297-.749-.69V3.544a6.47 6.47 0 0 0-3.285 1.36l.811.812a.75.75 0 0 1-1.06 1.06l-.812-.811A6.47 6.47 0 0 0 3.521 9.5h1.227l.076.004a.75.75 0 0 1 0 1.492L4.748 11h-1.17a6.5 6.5 0 0 0 1.712 3.48l.228.227l.052.056a.75.75 0 0 1-1.088 1.03A8 8 0 0 1 10 2m3.196 4.102a.502.502 0 0 1 .74.64v.002l-.001.002l-.005.009l-.02.037l-.078.138a195 195 0 0 1-1.15 2.026a80 80 0 0 1-.947 1.61c-.141.232-.271.44-.38.603a3 3 0 0 1-.295.391a1.5 1.5 0 0 1-2.12-2.12c.123-.124.464-.4.865-.718c.415-.33.93-.73 1.432-1.118a347 347 0 0 1 1.795-1.376l.121-.093l.032-.025l.01-.006z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="sim" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="modem" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title^="4g" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title^="lte" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title^="5g" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="sim" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="modem" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title^="4g" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title^="lte" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title^="5g" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M15 3a2 2 0 0 1 2 2v4.6a5.5 5.5 0 0 0-1-.393V5a1 1 0 1 0-2 0v4.022a5.5 5.5 0 0 0-1 .185V5a2 2 0 0 1 2-2M9.067 15.36c.089.565.263 1.101.51 1.595A2 2 0 0 1 8 15V8a2 2 0 1 1 4 0v1.6a5.5 5.5 0 0 0-1 .657V8a1 1 0 1 0-2 0v7q.001.192.067.36M5 9a2 2 0 0 1 2 2v4a2 2 0 1 1-4 0v-4a2 2 0 0 1 2-2m1 2a1 1 0 1 0-2 0v4a1 1 0 1 0 2 0zm13 3.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4.024-2.64a.494.494 0 0 0-.952 0l-.477 1.532H12c-.484 0-.686.647-.294.944l1.25.947l-.477 1.532c-.15.48.378.88.77.583l1.25-.947l1.25.947c.392.297.92-.103.77-.583l-.477-1.532l1.25-.947c.392-.297.19-.944-.294-.944h-1.545z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="sim" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="modem" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title^="4g" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title^="lte" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title^="5g" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M15 3a2 2 0 0 1 2 2v4.6a5.5 5.5 0 0 0-2.5-.6a5.5 5.5 0 0 0-1.5.207V5a2 2 0 0 1 2-2M9 14.5c0 .882.208 1.716.577 2.455A2 2 0 0 1 8 15V8a2 2 0 1 1 4 0v1.6a5.5 5.5 0 0 0-3 4.9M7 11a2 2 0 1 0-4 0v4a2 2 0 1 0 4 0zm12 3.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-4.024-2.64a.494.494 0 0 0-.952 0l-.477 1.532H12c-.484 0-.686.647-.294.944l1.25.947l-.477 1.532c-.15.48.378.88.77.583l1.25-.947l1.25.947c.392.297.92-.103.77-.583l-.477-1.532l1.25-.947c.392-.297.19-.944-.294-.944h-1.545z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Routing"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="routing" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Routing"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="routing" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M14.314 3.146a.5.5 0 0 0-.707.708L16.253 6.5H12A2.5 2.5 0 0 0 9.5 9v2A1.5 1.5 0 0 1 8 12.5h-.041a3 3 0 1 0 0 1H8a2.5 2.5 0 0 0 2.5-2.5V9A1.5 1.5 0 0 1 12 7.5h4.253l-2.646 2.646a.5.5 0 1 0 .707.707l3.5-3.5a.5.5 0 0 0 0-.707zM7 13a2 2 0 1 1-4 0a2 2 0 0 1 4 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Routing"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="routing" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M14.314 3.146a.5.5 0 0 0-.707.708L16.253 6.5H12A2.5 2.5 0 0 0 9.5 9v2A1.5 1.5 0 0 1 8 12.5h-.041a3 3 0 1 0 0 1H8a2.5 2.5 0 0 0 2.5-2.5V9A1.5 1.5 0 0 1 12 7.5h4.253l-2.646 2.646a.5.5 0 1 0 .707.707l3.5-3.5a.5.5 0 0 0 0-.707z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="Wake_on_LAN" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="wol" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="timer" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title*="Wake_on_LAN" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="wol" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="timer" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M13.264 2.078a.5.5 0 1 0-.523.852c2.258 1.384 4.12 3.414 4.26 7.09A.5.5 0 0 0 18 9.982c-.157-4.099-2.278-6.398-4.736-7.904m-1.178 2.65a.5.5 0 0 1 .694-.134c1.607 1.085 2.715 2.638 2.888 4.424q.024.24.024.487a.5.5 0 0 1-1 0q0-.198-.02-.39c-.136-1.418-1.024-2.728-2.452-3.693a.5.5 0 0 1-.134-.694m-7.006.71a5.16 5.16 0 0 0-2.614 6.811l1.223 2.749l.09 2.32a.75.75 0 0 0 1.054.656l9.727-4.33a.75.75 0 0 0 .218-1.223l-1.664-1.619l-1.224-2.749a5.16 5.16 0 0 0-6.81-2.614m-1.7 6.404a4.158 4.158 0 0 1 7.596-3.382l1.302 2.925l1.538 1.495l-9.052 4.03l-.083-2.143zm7.298 6.034a1.49 1.49 0 0 1-1.848-.54l2.685-1.194a1.49 1.49 0 0 1-.837 1.734'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title*="Wake_on_LAN" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="wol" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="timer" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2.466 12.25a5.158 5.158 0 0 1 9.424-4.197l1.224 2.749l1.664 1.619a.75.75 0 0 1-.218 1.222l-9.727 4.331a.75.75 0 0 1-1.054-.656l-.09-2.32zm6.364 5.087a1.49 1.49 0 0 0 2.685-1.195zm3.256-12.609a.5.5 0 0 1 .694-.134c1.608 1.085 2.715 2.638 2.888 4.424q.024.24.024.487a.5.5 0 1 1-1 0a4 4 0 0 0-.02-.39c-.136-1.418-1.024-2.728-2.452-3.693a.5.5 0 0 1-.134-.694m.49-2.485a.5.5 0 0 1 .688-.165c2.458 1.506 4.58 3.805 4.736 7.904a.5.5 0 0 1-1 .038c-.14-3.676-2-5.706-4.259-7.09a.5.5 0 0 1-.165-.687'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Images"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="plugin" i] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title*="kit" i] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Images"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="plugin" i] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title*="kit" i] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M13.577 2.38a3 3 0 0 0-2.154 0L7.962 3.71A1.5 1.5 0 0 0 7 5.11v1.101a4 4 0 0 1 1 0v-1.1a.5.5 0 0 1 .32-.467l3.462-1.331a2 2 0 0 1 1.436 0l3.461 1.331a.5.5 0 0 1 .32.467v7.78a.5.5 0 0 1-.32.466l-2.68 1.031v.503q0 .307-.072.596l3.111-1.196a1.5 1.5 0 0 0 .962-1.4V5.11a1.5 1.5 0 0 0-.962-1.4zm2.514 2.915a.5.5 0 0 0-.646-.287l-2.766 1.064a.5.5 0 0 1-.359 0L9.554 5.008a.5.5 0 1 0-.359.933l2.766 1.064a1.5 1.5 0 0 0 1.077 0l2.766-1.064a.5.5 0 0 0 .287-.646m-5 5a.5.5 0 0 0-.646-.287L7.5 11.141l-2.946-1.133a.5.5 0 0 0-.359.934L7 12.02v2.49a.5.5 0 1 0 1 0v-2.49l2.804-1.078a.5.5 0 0 0 .287-.647M8.577 7.38a3 3 0 0 0-2.154 0L2.962 8.71A1.5 1.5 0 0 0 2 10.11v4.78a1.5 1.5 0 0 0 .962 1.4l3.46 1.332a3 3 0 0 0 2.155 0l3.461-1.331a1.5 1.5 0 0 0 .961-1.4v-4.78a1.5 1.5 0 0 0-.961-1.4zm-1.795.933a2 2 0 0 1 1.436 0l3.461 1.331a.5.5 0 0 1 .32.467v4.78a.5.5 0 0 1-.32.466L8.218 16.69a2 2 0 0 1-1.436 0L3.32 15.357a.5.5 0 0 1-.32-.465v-4.78a.5.5 0 0 1 .32-.467z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Images"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="plugin" i] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title*="kit" i] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M13.577 2.378a3 3 0 0 0-2.154 0L7.962 3.709A1.5 1.5 0 0 0 7 5.109v1.1a4 4 0 0 1 1.936.235l3.46 1.332A2.5 2.5 0 0 1 14 10.109v4.78a2.5 2.5 0 0 1-.072.596l3.111-1.196a1.5 1.5 0 0 0 .962-1.4v-7.78a1.5 1.5 0 0 0-.962-1.4zm1.868 2.154a.5.5 0 1 1 .359.934l-2.766 1.063a1.5 1.5 0 0 1-1.077 0L9.195 5.466a.5.5 0 0 1 .36-.934l2.765 1.064a.5.5 0 0 0 .36 0zM6.423 7.378a3 3 0 0 1 2.154 0l3.461 1.331c.58.223.961.78.961 1.4v4.78a1.5 1.5 0 0 1-.961 1.4L8.577 17.62a3 3 0 0 1-2.154 0l-3.461-1.33A1.5 1.5 0 0 1 2 14.89v-4.78a1.5 1.5 0 0 1 .962-1.4zm4.668 2.915a.5.5 0 0 0-.646-.287L7.5 11.139l-2.946-1.133a.5.5 0 1 0-.359.933L7 12.018v2.49a.5.5 0 0 0 1 0v-2.49l2.804-1.079a.5.5 0 0 0 .287-.646'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Overview"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Overview"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M6.5 10a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1zm0 2a.5.5 0 0 0 0 1h7a.5.5 0 0 0 0-1zM4 4a2 2 0 0 1 2-2h4.586a1.5 1.5 0 0 1 1.06.44l3.915 3.914A1.5 1.5 0 0 1 16 7.414V16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V8h-3.5A1.5 1.5 0 0 1 10 6.5V3zm5.5 4h3.293L11 3.207V6.5a.5.5 0 0 0 .5.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Overview"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 6.5V2H5.5A1.5 1.5 0 0 0 4 3.5v13A1.5 1.5 0 0 0 5.5 18h9a1.5 1.5 0 0 0 1.5-1.5V8h-4.5A1.5 1.5 0 0 1 10 6.5M6.5 10h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1m0 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1m0 2h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1 0-1M11 6.5V2.25L15.75 7H11.5a.5.5 0 0 1-.5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Networks"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Networks"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.825 6.12a.5.5 0 0 1 .055.705L2.159 10l2.72 3.175a.5.5 0 1 1-.759.65l-3-3.5a.5.5 0 0 1 0-.65l3-3.5a.5.5 0 0 1 .705-.055m10.295 7.055a.5.5 0 0 0 .76.65l3-3.5a.5.5 0 0 0 0-.65l-3-3.5a.5.5 0 1 0-.76.65L17.842 10zM6 11a1 1 0 1 0 0-2a1 1 0 0 0 0 2m5-1a1 1 0 1 1-2 0a1 1 0 0 1 2 0m3 1a1 1 0 1 0 0-2a1 1 0 0 0 0 2'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Networks"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4.707 6.156a.75.75 0 0 1 .137 1.051L2.696 10l2.148 2.793a.75.75 0 1 1-1.188.914l-2.5-3.25a.75.75 0 0 1 0-.914l2.5-3.25a.75.75 0 0 1 1.051-.137m10.449 6.637a.75.75 0 1 0 1.188.914l2.5-3.25a.75.75 0 0 0 0-.914l-2.5-3.25a.75.75 0 1 0-1.188.914L17.304 10zM6 11a1 1 0 1 0 0-2a1 1 0 0 0 0 2m5-1a1 1 0 1 1-2 0a1 1 0 0 1 2 0m3 1a1 1 0 1 0 0-2a1 1 0 0 0 0 2'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Volumes"] .menu-icon, .main-left .nav li.slide .slide-menu li a[data-title="Disk_Man"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Volumes"] .menu-icon:before, .main-left .nav li.slide .slide-menu li a[data-title="Disk_Man"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M13 9a1 1 0 1 1-2 0a1 1 0 0 1 2 0m2 1a1 1 0 1 0 0-2a1 1 0 0 0 0 2M2 8.5A2.5 2.5 0 0 1 4.5 6h11A2.5 2.5 0 0 1 18 8.5v3a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 11.5zM4.5 7A1.5 1.5 0 0 0 3 8.5v3A1.5 1.5 0 0 0 4.5 13h11a1.5 1.5 0 0 0 1.5-1.5v-3A1.5 1.5 0 0 0 15.5 7z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Volumes"] .menu-icon:after, .main-left .nav li.slide .slide-menu li a[data-title="Disk_Man"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2 8.5A2.5 2.5 0 0 1 4.5 6h11A2.5 2.5 0 0 1 18 8.5v3a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 2 11.5zM13 9a1 1 0 1 0-2 0a1 1 0 0 0 2 0m3 0a1 1 0 1 0-2 0a1 1 0 0 0 2 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Firewall"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Firewall"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.5c0 3.891-2.307 6.73-6.82 8.467a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421m-.124 1.262A16 16 0 0 1 4 5.428V9.5c0 3.392 1.968 5.863 6 7.463c4.032-1.6 6-4.071 6-7.463V5.428a16 16 0 0 1-5.6-2.082l-.4-.249z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Firewall"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.723 2.084a.5.5 0 0 1 .554 0a15.05 15.05 0 0 0 6.294 2.421A.5.5 0 0 1 17 5v4.5c0 3.891-2.307 6.73-6.82 8.467a.5.5 0 0 1-.36 0C5.308 16.23 3 13.39 3 9.5V5a.5.5 0 0 1 .43-.495a15.05 15.05 0 0 0 6.293-2.421'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="DNS"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="DNS"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 18a8 8 0 1 0 0-16a8 8 0 0 0 0 16m0-15c.657 0 1.407.59 2.022 1.908c.217.466.406 1.002.559 1.592H7.419c.153-.59.342-1.126.56-1.592C8.592 3.59 9.342 3 10 3M7.072 4.485A10.5 10.5 0 0 0 6.389 6.5H3.936a7.02 7.02 0 0 1 3.778-3.118c-.241.33-.456.704-.642 1.103M6.192 7.5A16 16 0 0 0 6 10c0 .87.067 1.712.193 2.5H3.46A7 7 0 0 1 3 10c0-.88.163-1.724.46-2.5zm.197 6c.176.743.407 1.422.683 2.015c.186.399.401.773.642 1.103A7.02 7.02 0 0 1 3.936 13.5zm1.03 0h5.162a9.3 9.3 0 0 1-.56 1.592C11.408 16.41 10.658 17 10 17s-1.407-.59-2.022-1.908A9.3 9.3 0 0 1 7.42 13.5m5.375-1H7.206A15 15 0 0 1 7 10c0-.883.073-1.725.206-2.5h5.588c.133.775.206 1.617.206 2.5s-.073 1.725-.206 2.5m.817 1h2.453a7.02 7.02 0 0 1-3.778 3.118c.241-.33.456-.704.642-1.103c.276-.593.507-1.272.683-2.015m2.93-1h-2.734c.126-.788.193-1.63.193-2.5s-.067-1.712-.193-2.5h2.733c.297.776.46 1.62.46 2.5s-.163 1.724-.46 2.5m-4.255-9.118A7.02 7.02 0 0 1 16.064 6.5H13.61a10.5 10.5 0 0 0-.683-2.015a6.6 6.6 0 0 0-.642-1.103'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="DNS"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.987 4.18a12.2 12.2 0 0 0-.669 2.32h5.364a12.2 12.2 0 0 0-.67-2.32c-.301-.733-.648-1.294-1.008-1.663C10.646 2.149 10.307 2 10 2s-.646.149-1.004.517c-.36.37-.707.93-1.009 1.663m-.096-1.899c-.314.426-.59.941-.828 1.518c-.32.78-.58 1.694-.762 2.701H2.804a8.02 8.02 0 0 1 5.087-4.219m4.219 0c.313.426.59.941.827 1.518c.32.78.58 1.694.762 2.701h3.497a8.02 8.02 0 0 0-5.087-4.219M17.602 7.5H13.85c.098.795.15 1.634.15 2.5s-.052 1.705-.15 2.5h3.752A8 8 0 0 0 18 10a8 8 0 0 0-.398-2.5m-.406 6h-3.497c-.182 1.007-.441 1.922-.762 2.7a7.2 7.2 0 0 1-.827 1.519a8.02 8.02 0 0 0 5.086-4.219M10 18c.307 0 .646-.149 1.004-.517c.36-.37.707-.93 1.008-1.663a12.2 12.2 0 0 0 .67-2.32H7.318c.168.873.397 1.657.67 2.32c.301.733.648 1.294 1.008 1.663c.358.368.697.517 1.004.517m-2.11-.281A8.02 8.02 0 0 1 2.805 13.5h3.497c.182 1.007.441 1.922.762 2.7c.237.578.514 1.093.828 1.519M2.399 12.5H6.15A21 21 0 0 1 6 10c0-.866.052-1.705.15-2.5H2.398A8 8 0 0 0 2 10c0 .873.14 1.713.398 2.5M7 10c0-.875.056-1.715.158-2.5h5.684c.102.785.158 1.625.158 2.5s-.056 1.714-.158 2.5H7.158A19 19 0 0 1 7 10'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Diagnostics"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Diagnostics"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.497 16.803L15.253 12H14.25q-.184 0-.36-.044l-3.865 3.902l-3.863-3.902Q5.986 12 5.8 12H4.799l4.755 4.803c.26.263.682.263.942 0m6.227-12.49a4.42 4.42 0 0 1 .978 4.7A2 2 0 0 0 17.5 9h-.889a3.415 3.415 0 0 0-.598-3.984A3.306 3.306 0 0 0 11.3 5l-.951.963a.5.5 0 0 1-.711 0l-.96-.97a3.3 3.3 0 0 0-4.706-.016C2.899 6.061 2.713 7.711 3.42 9H2.5q-.09 0-.18.01a4.4 4.4 0 0 1 .941-4.736a4.3 4.3 0 0 1 6.127.016l.605.61l.596-.603l.109-.106a4.306 4.306 0 0 1 6.026.121M7.962 6.307a.5.5 0 0 0-.922-.004L5.47 10H2.5a.5.5 0 0 0 0 1h3.3a.5.5 0 0 0 .46-.304l1.235-2.907l2.043 4.903a.5.5 0 0 0 .886.073l2.143-3.429l1.307 1.493a.5.5 0 0 0 .376.171h3.25a.5.5 0 0 0 0-1h-3.023l-1.6-1.83a.5.5 0 0 0-.801.065l-1.987 3.179z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Diagnostics"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.388 4.29a4.3 4.3 0 0 0-6.127-.016a4.4 4.4 0 0 0-.94 4.737Q2.408 9 2.5 9h2.308L6.12 5.913a1.5 1.5 0 0 1 2.765.01l1.383 3.319l.96-1.537a1.5 1.5 0 0 1 2.4-.193L14.932 9H17.5q.103 0 .202.014a4.42 4.42 0 0 0-.978-4.702a4.305 4.305 0 0 0-6.135-.015l-.596.603zm.166 12.513L4.799 12H5.8a1.5 1.5 0 0 0 1.38-.913l.306-.72l1.13 2.71a1.5 1.5 0 0 0 2.656.218l1.43-2.287l.42.48A1.5 1.5 0 0 0 14.25 12h1.004l-4.757 4.803a.66.66 0 0 1-.943 0M7.962 6.308a.5.5 0 0 0-.922-.004L5.47 10H2.5a.5.5 0 0 0 0 1h3.3a.5.5 0 0 0 .46-.304l1.235-2.907l2.043 4.903a.5.5 0 0 0 .886.073l2.143-3.429l1.307 1.493a.5.5 0 0 0 .376.171h3.25a.5.5 0 0 0 0-1h-3.023l-1.6-1.83a.5.5 0 0 0-.801.065l-1.987 3.179z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Startup"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Startup"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M17.22 8.687a1.498 1.498 0 0 1 0 2.626l-9.997 5.499A1.5 1.5 0 0 1 5 15.499V4.501a1.5 1.5 0 0 1 2.223-1.313zm-.482 1.75a.5.5 0 0 0 0-.875L6.741 4.063A.5.5 0 0 0 6 4.501v10.998a.5.5 0 0 0 .741.438z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Startup"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M17.222 8.685a1.5 1.5 0 0 1 0 2.628l-10 5.498A1.5 1.5 0 0 1 5 15.496V4.502a1.5 1.5 0 0 1 2.223-1.314z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Scheduled_Tasks"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Scheduled_Tasks"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16m0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14m-.5 2a.5.5 0 0 1 .492.41L10 5.5V10h2.5a.5.5 0 0 1 .09.992L12.5 11h-3a.5.5 0 0 1-.492-.41L9 10.5v-5a.5.5 0 0 1 .5-.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Scheduled_Tasks"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16m-.5 3a.5.5 0 0 0-.5.5v5l.008.09A.5.5 0 0 0 9.5 11h3l.09-.008A.5.5 0 0 0 12.5 10H10V5.5l-.008-.09A.5.5 0 0 0 9.5 5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Mount_Points"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Mount_Points"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2 12c0-.398.08-.791.237-1.156l2.414-5.632A2 2 0 0 1 6.489 4h7.104c.742 0 1.43.41 1.763 1.088c.571 1.162 1.633 3.424 2.454 5.774c.13.369.19.754.19 1.138v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2zm11.593-7H6.49a1 1 0 0 0-.919.606l-1.894 4.42Q3.834 10 4 10h12q.231 0 .447.05a57 57 0 0 0-1.988-4.521a.96.96 0 0 0-.866-.529M16 15a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1zm-1.25-1.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Mount_Points"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4 10c-.588 0-1.136.169-1.599.461l2.25-5.249A2 2 0 0 1 6.489 4h7.104c.742 0 1.43.41 1.763 1.088c.544 1.106 1.53 3.207 2.333 5.432A3 3 0 0 0 16 10zm12 1a2 2 0 0 1 2 2v1a2 2 0 0 1-2 2H3.987A2 2 0 0 1 2 14v-1a2 2 0 0 1 2-2zm-.5 3.25a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Backup_/_Flash_Firmware"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Backup_/_Flash_Firmware"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M11.414 3.635a.5.5 0 0 0 0-.707L9.293.807a.5.5 0 0 0-.707.707l.997.997a7.5 7.5 0 0 0-4.075 13.495a.5.5 0 0 0 .6-.8A6.5 6.5 0 0 1 10.066 3.5q.037 0 .073-.005L8.586 5.049a.5.5 0 0 0 .707.707zM8.586 16.363a.5.5 0 0 0 0 .707l2.121 2.121a.5.5 0 0 0 .707-.707l-.997-.997a7.5 7.5 0 0 0 4.075-13.495a.5.5 0 1 0-.6.8q.369.277.704.61A6.5 6.5 0 0 1 9.934 16.5l-.074.004l1.554-1.554a.5.5 0 1 0-.707-.707z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Backup_/_Flash_Firmware"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9.885 3.75a6.25 6.25 0 0 0-3.628 11.256a.75.75 0 0 1-.9 1.2a7.75 7.75 0 0 1 3.99-13.93l-.584-.586A.75.75 0 0 1 9.823.63l2.122 2.121a.75.75 0 0 1 0 1.06L9.823 5.934a.75.75 0 0 1-1.06-1.06zm.23 12.498a6.25 6.25 0 0 0 3.628-11.256a.75.75 0 0 1 .9-1.2q.439.33.837.727a7.75 7.75 0 0 1-4.828 13.203l.585.585a.75.75 0 1 1-1.06 1.061l-2.122-2.121a.75.75 0 0 1 0-1.06l2.122-2.122a.75.75 0 1 1 1.06 1.06z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Terminal"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Terminal"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M5.646 9.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L7.293 11.5L5.646 9.854a.5.5 0 0 1 0-.708M14.5 13h-5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1M2.996 5.5a2.5 2.5 0 0 1 2.5-2.5h9a2.5 2.5 0 0 1 2.5 2.5V6H17v1h-.003v7.5a2.5 2.5 0 0 1-2.5 2.5h-9a2.5 2.5 0 0 1-2.5-2.5zm13 .5v-.5a1.5 1.5 0 0 0-1.5-1.5h-9a1.5 1.5 0 0 0-1.5 1.5V6zm-12 1v7.5a1.5 1.5 0 0 0 1.5 1.5h9a1.5 1.5 0 0 0 1.5-1.5V7z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Terminal"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M5.646 9.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1 0 .708l-2 2a.5.5 0 0 1-.708-.708L7.293 11.5L5.646 9.854a.5.5 0 0 1 0-.708M14.5 13h-5a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1M2.996 5.5a2.5 2.5 0 0 1 2.5-2.5h9a2.5 2.5 0 0 1 2.5 2.5V6H17v1h-.003v7.5a2.5 2.5 0 0 1-2.5 2.5h-9a2.5 2.5 0 0 1-2.5-2.5zm1 1.5v7.5a1.5 1.5 0 0 0 1.5 1.5h9a1.5 1.5 0 0 0 1.5-1.5V7z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Processes"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Processes"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v2.03a4.5 4.5 0 0 0-1-.004V7H4v7a2 2 0 0 0 2 2h3.492a2.5 2.5 0 0 0-.443 1H6a3 3 0 0 1-3-3zm10.044 3.587l-1.44-1.44a.5.5 0 0 0-.708.707l1.578 1.577q.232-.447.57-.844M6 4a2 2 0 0 0-2 2h12a2 2 0 0 0-2-2zm3.104 4.854a.5.5 0 1 0-.708-.708l-2.5 2.5a.5.5 0 0 0 0 .708l2.5 2.5a.5.5 0 0 0 .708-.708L6.957 11zm7.788.17c.366.042.471.48.21.742l-.975.975a1.507 1.507 0 1 0 2.132 2.132l.975-.975c.261-.261.7-.156.742.21a3.518 3.518 0 0 1-4.676 3.723l-2.726 2.727a1.507 1.507 0 1 1-2.132-2.132l2.726-2.726a3.518 3.518 0 0 1 3.724-4.676'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Processes"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3 6a3 3 0 0 1 3-3h8a3 3 0 0 1 3 3v2.03a4.5 4.5 0 0 0-1-.004V7H4v7a2 2 0 0 0 2 2h3.492a2.5 2.5 0 0 0-.443 1H6a3 3 0 0 1-3-3zm10.044 3.587l-1.44-1.44a.5.5 0 0 0-.708.707l1.578 1.577q.232-.447.57-.844m-3.94-.733a.5.5 0 1 0-.708-.708l-2.5 2.5a.5.5 0 0 0 0 .708l2.5 2.5a.5.5 0 0 0 .708-.708L6.957 11zm7.788.17c.366.042.471.48.21.742l-.975.975a1.507 1.507 0 1 0 2.132 2.132l.975-.975c.261-.261.7-.156.742.21a3.518 3.518 0 0 1-4.676 3.723l-2.726 2.727a1.507 1.507 0 1 1-2.132-2.132l2.726-2.726a3.518 3.518 0 0 1 3.724-4.676'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Channel_Analysis"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Channel_Analysis"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='m14.497 3.382l-.722.722A7 7 0 1 0 14.95 14.95a.5.5 0 0 1 .707.707a8 8 0 1 1-1.16-12.275m-2.935 2.935l.751-.751a5.002 5.002 0 0 0-5.849 7.97a5 5 0 0 0 7.072 0a.5.5 0 1 0-.708-.708a4 4 0 1 1-1.266-6.511m4.292-2.17a.5.5 0 0 1 0 .707L10.966 9.74a1.001 1.001 0 1 1-.707-.707l4.887-4.888a.5.5 0 0 1 .708 0M17 13.5a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0M16.5 7a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1m1.5 4.25a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0m-.5-2a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1m-2.5 2a.5.5 0 1 1-1 0a.5.5 0 0 1 1 0m-.5-2a.5.5 0 1 0 0-1a.5.5 0 0 0 0 1'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Channel_Analysis"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M14.284 3.241L13.19 4.335a6.5 6.5 0 1 0 1.406 10.261a.75.75 0 0 1 1.06 1.06a8 8 0 1 1-1.372-12.415m-3.296 3.296l1.163-1.163a5.102 5.102 0 0 0-5.757 8.232a5.1 5.1 0 0 0 7.212 0a.75.75 0 1 0-1.06-1.06a3.6 3.6 0 1 1-1.558-6.009M15.78 4.22a.75.75 0 0 1 0 1.06l-4.33 4.331q.049.187.05.389a1.5 1.5 0 1 1-1.111-1.45l4.33-4.33a.75.75 0 0 1 1.061 0m1.47 9.03a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0M16.5 7.5a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5m1.5 3.75a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m-.75-1.75a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5M15 11.25a.75.75 0 1 1-1.5 0a.75.75 0 0 1 1.5 0m-.75-1.75a.75.75 0 1 0 0-1.5a.75.75 0 0 0 0 1.5'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Realtime_Graphs"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Realtime_Graphs"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4 2a2 2 0 0 0-2 2v4h1V4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v4h1V4a2 2 0 0 0-2-2zM2 13v-2h1v2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2h1v2a2 2 0 0 1-2 2h-3v2h1.5a.5.5 0 0 1 0 1h-9a.5.5 0 0 1 0-1H7v-2H4a2 2 0 0 1-2-2m10 2H8v2h4zM7.977 4.35a.5.5 0 0 0-.941-.036L5.162 9H2.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .465-.314L7.442 5.99l2.08 6.658a.5.5 0 0 0 .925.075L13 7.618l1.052 2.106A.5.5 0 0 0 14.5 10h3a.5.5 0 1 0 0-1h-2.691l-1.362-2.724a.5.5 0 0 0-.894 0l-2.446 4.891z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Realtime_Graphs"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M2 4a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4h-2.573l-1.085-2.17a1.5 1.5 0 0 0-2.684 0l-1.336 2.672l-1.39-4.45a1.5 1.5 0 0 0-2.825-.109L4.485 8H2zm12.5 7H18v2a2 2 0 0 1-2 2h-3v2h1.5a.5.5 0 0 1 0 1h-9a.5.5 0 0 1 0-1H7v-2H4a2 2 0 0 1-2-2v-2h3.5a1.5 1.5 0 0 0 1.394-.943l.433-1.083l1.241 3.973a1.5 1.5 0 0 0 2.774.224L13 9.854l.158.317A1.5 1.5 0 0 0 14.5 11M12 17v-2H8v2zM7.977 4.35a.5.5 0 0 0-.941-.036L5.162 9H2.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 .465-.314L7.442 5.99l2.08 6.658a.5.5 0 0 0 .925.075L13 7.618l1.053 2.106A.5.5 0 0 0 14.5 10h3a.5.5 0 0 0 0-1h-2.691l-1.362-2.724a.5.5 0 0 0-.894 0l-2.446 4.891z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="AmneziaWG"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="AmneziaWG"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M11.5 9a1.5 1.5 0 0 1-1 1.415V12.5a.5.5 0 0 1-1 0v-2.085A1.5 1.5 0 1 1 11.5 9m-1.223-6.916a.5.5 0 0 0-.554 0a15.05 15.05 0 0 1-6.294 2.421A.5.5 0 0 0 3 5v4.5c0 3.891 2.307 6.73 6.82 8.467a.5.5 0 0 0 .36 0C14.693 16.23 17 13.39 17 9.5V5a.5.5 0 0 0-.43-.495a15.05 15.05 0 0 1-6.293-2.421M4 5.428a16 16 0 0 0 5.6-2.082l.4-.249l.4.249A16 16 0 0 0 16 5.428V9.5c0 3.392-1.968 5.863-6 7.463c-4.032-1.6-6-4.071-6-7.463z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="AmneziaWG"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M10.277 2.084a.5.5 0 0 0-.554 0a15.05 15.05 0 0 1-6.294 2.421A.5.5 0 0 0 3 5v4.5c0 3.891 2.307 6.73 6.82 8.467a.5.5 0 0 0 .36 0C14.693 16.23 17 13.39 17 9.5V5a.5.5 0 0 0-.43-.495a15.05 15.05 0 0 1-6.293-2.421M11.5 9a1.5 1.5 0 0 1-1 1.415V12.5a.5.5 0 0 1-1 0v-2.085A1.5 1.5 0 1 1 11.5 9'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Release_RAM"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Release_RAM"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M17.91 2.182a.5.5 0 0 1 0 .707L12.587 8.21a4.5 4.5 0 0 1-.334 5.991l-.664.664l-2.09 3.483a.5.5 0 0 1-.781.096l-7.072-7.071a.5.5 0 0 1 .097-.782l3.483-2.09l.663-.664a4.5 4.5 0 0 1 5.991-.334l5.323-5.322a.5.5 0 0 1 .707 0M6.243 8.899l4.95 4.95l.353-.354a3.5 3.5 0 1 0-4.95-4.95zm-.785.63l-2.652 1.59l6.166 6.166l1.59-2.652z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Release_RAM"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M17.91 2.182a.5.5 0 0 1 0 .707L13.298 7.5a5.6 5.6 0 0 0-.707-.707l4.612-4.612a.5.5 0 0 1 .707 0m-5.657 5.656a4.5 4.5 0 0 0-6.364 0l-.421.422l6.364 6.364l.421-.422a4.5 4.5 0 0 0 0-6.364m-10.51 2.754L4.644 8.85l.002.002l6.5 6.5a.5.5 0 0 0 .103.08L9.5 18.348a.5.5 0 0 1-.781.096l-7.072-7.071a.5.5 0 0 1 .097-.782'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Advanced_plus"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Advanced_plus"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M9 6.5a4.5 4.5 0 0 1 6.353-4.102a.5.5 0 0 1 .148.809L13.208 5.5L14.5 6.793L16.793 4.5a.5.5 0 0 1 .81.147c.255.566.397 1.194.397 1.853c0 1.162-.44 2.221-1.163 3.02a5.5 5.5 0 0 0-1.051-.369a3.49 3.49 0 0 0 1.14-3.37l-2.072 2.073a.5.5 0 0 1-.707 0l-2-2a.5.5 0 0 1 0-.708l2.073-2.072a3.5 3.5 0 0 0-4.106 4.312a.5.5 0 0 1-.125.473l-6.612 6.854a1.357 1.357 0 0 0 1.942 1.896l4.057-4.11a5.5 5.5 0 0 0-.372 1.8l-2.973 3.012a2.357 2.357 0 0 1-3.374-3.293L9.083 7.36A4.5 4.5 0 0 1 9 6.5m1.635 7.42a2 2 0 0 0 1.43-2.478l-.155-.557q.382-.293.821-.497l.338.358a2 2 0 0 0 2.91.001l.324-.344q.448.212.835.519l-.126.422a2 2 0 0 0 1.456 2.519l.349.082a4.7 4.7 0 0 1 .01 1.017l-.46.118a2 2 0 0 0-1.431 2.478l.156.556q-.383.295-.822.498l-.338-.358a2 2 0 0 0-2.909-.002l-.325.345a4.3 4.3 0 0 1-.835-.518l.127-.423a2 2 0 0 0-1.456-2.52l-.35-.082a4.7 4.7 0 0 1-.01-1.016zm2.865.58a1 1 0 1 0 2 0a1 1 0 0 0-2 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Advanced_plus"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M13.5 2a4.5 4.5 0 0 0-4.417 5.36l-6.426 6.658a2.357 2.357 0 0 0 3.374 3.293L9.004 14.3a5.5 5.5 0 0 1 7.833-4.78a4.51 4.51 0 0 0 1.05-4.03a.5.5 0 0 0-.841-.242L14.5 7.793L12.208 5.5l2.545-2.545a.5.5 0 0 0-.242-.84A4.5 4.5 0 0 0 13.501 2m-2.865 11.92a2 2 0 0 0 1.43-2.478l-.155-.557q.382-.293.821-.497l.338.358a2 2 0 0 0 2.91.001l.324-.344q.448.212.835.519l-.126.422a2 2 0 0 0 1.456 2.519l.349.082a4.7 4.7 0 0 1 .01 1.017l-.46.118a2 2 0 0 0-1.431 2.478l.156.556q-.383.295-.822.498l-.338-.358a2 2 0 0 0-2.909-.002l-.325.345a4.3 4.3 0 0 1-.835-.518l.127-.423a2 2 0 0 0-1.456-2.52l-.35-.082a4.7 4.7 0 0 1-.01-1.016zm2.865.58a1 1 0 1 0 2 0a1 1 0 0 0-2 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Athena_LED_Ctrl"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Athena_LED_Ctrl"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M4 2a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3v2H5.5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1H13v-2h3a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2zm8 13v2H8v-2zM3 4a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Athena_LED_Ctrl"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M3.5 2A1.5 1.5 0 0 0 2 3.5v10A1.5 1.5 0 0 0 3.5 15H7v2H5.5a.5.5 0 0 0 0 1h9a.5.5 0 0 0 0-1H13v-2h3.5a1.5 1.5 0 0 0 1.5-1.5v-10A1.5 1.5 0 0 0 16.5 2zM12 15v2H8v-2z'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Scheduled_Reboot"] .menu-icon { + display: inline-flex; +} + +.main-left .nav li.slide .slide-menu li a[data-title="Scheduled_Reboot"] .menu-icon:before { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7.5 2a.5.5 0 0 0 0 1h4a.5.5 0 0 0 0-1zm7.656 1.929a.5.5 0 1 0-.707.707l1.414 1.414a.5.5 0 1 0 .707-.707zM9.5 6a.5.5 0 0 0-.5.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-.5-.5m0 12a7 7 0 1 0 0-14a7 7 0 0 0 0 14m0-1a6 6 0 1 1 0-12a6 6 0 0 1 0 12'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li a[data-title="Scheduled_Reboot"] .menu-icon:after { + --icon-data: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20' fill='none'%3E%3Cpath fill='currentColor' d='M7 2.5a.5.5 0 0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5m7.45 1.429a.5.5 0 0 1 .706 0l1.414 1.414a.5.5 0 1 1-.707.707L14.45 4.636a.5.5 0 0 1 0-.707M16.5 11a7 7 0 1 1-14 0a7 7 0 0 1 14 0M9 6.5v5a.5.5 0 0 0 1 0v-5a.5.5 0 0 0-1 0'/%3E%3C/svg%3E"); +} + +.main-left .nav li.slide .slide-menu li:hover { + background: var(--fluent-sidebar-item-hover); +} + +.main-left .nav li.slide .slide-menu li:hover:before { + background-color: color-mix(in srgb, var(--fluent-primary) 30%, transparent); +} + +.main-left .nav li.slide .slide-menu li:hover a { + color: var(--fluent-text); +} + +.main-left .nav li.slide .slide-menu li:hover a .menu-icon:before { + opacity: 0; +} + +.main-left .nav li.slide .slide-menu li:hover a .menu-icon:after { + opacity: 1; +} + +.main-left .nav li.slide .slide-menu li.active { + background: var(--fluent-sidebar-item-active); + color: var(--fluent-text); +} + +.main-left .nav li.slide .slide-menu li.active:before { + background-color: var(--fluent-primary); +} + +.main-left .nav li.slide .slide-menu li.active a { + color: var(--fluent-primary); + font-weight: var(--fluent-font-weight-semibold); +} + +.main-left .nav li.slide .slide-menu li.active a .menu-icon { + color: var(--fluent-primary); +} + +.main-left .nav li.slide .slide-menu li.active a .menu-icon:before { + opacity: 0; +} + +.main-left .nav li.slide .slide-menu li.active a .menu-icon:after { + opacity: 1; +} + +.main-left .nav li.slide .slide-menu li.active:hover { + background: var(--fluent-sidebar-item-active); +} + +.main-left .nav li.slide .slide-menu li.active:hover:before { + background-color: var(--fluent-primary); +} + +.main-left .nav li .menu:after { + inset-inline-end: var(--fluent-space-sm); + transition: transform var(--fluent-duration-fast) var(--fluent-easing-standard),background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-chevron-down-20-regular: url("data:image/svg+xml,"); + width: 14px; + height: 14px; + -webkit-mask-image: var(--fluent-mask-chevron-down-20-regular); + mask-image: var(--fluent-mask-chevron-down-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + position: absolute; + top: 50%; + transform: translateY(-50%) rotate(-90deg); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +.main-left .nav li .menu.active:after { + transform: translateY(-50%) rotate(0); + background-color: var(--fluent-primary) !important; +} + +html[dir="rtl"] .main-left .nav li .menu:after, body[data-fluent-dir="rtl"] .main-left .nav li .menu:after { + transform: translateY(-50%) rotate(90deg); +} + +html[dir="rtl"] .main-left .nav li .menu.active:after, body[data-fluent-dir="rtl"] .main-left .nav li .menu.active:after { + transform: translateY(-50%) rotate(0); +} + +body[data-sidebar-state="collapsed"] .main-left { + width: var(--fluent-sidebar-collapsed-width, 48px); + scrollbar-width: none; + overflow-x: visible; +} + +body[data-sidebar-state="collapsed"] .main-left::-webkit-scrollbar { + display: none; +} + +body[data-sidebar-state="collapsed"] .main-left .sidenav-header { + justify-content: center; + gap: 0; + padding: 0; +} + +body[data-sidebar-state="collapsed"] .main-left .sidenav-header .brand { + display: none; +} + +body[data-sidebar-state="collapsed"] .main-left .sidenav-header .sidebar-collapse-toggle { + border-radius: 0; + justify-content: center; + width: 100%; + height: 48px; + margin-inline-start: 0; +} + +body[data-sidebar-state="collapsed"] .main-left .sidenav-header .sidebar-collapse-toggle:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-line-horizontal-3-20-regular: url("data:image/svg+xml,"); + width: 20px; + height: 20px; + -webkit-mask-image: var(--fluent-mask-line-horizontal-3-20-regular); + mask-image: var(--fluent-mask-line-horizontal-3-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-text-secondary) !important; +} + +body[data-sidebar-state="collapsed"] .main-right { + z-index: 1; + margin-inline-start: var(--fluent-sidebar-collapsed-width, 48px); + position: relative; +} + +body[data-sidebar-state="collapsed"] .main-left .nav { + padding-inline: 0; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > .slide-menu.active { + display: none; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li { + position: relative; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a:first-child { + justify-content: flex-start; + padding: 0; + padding-inline-start: 14px; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a:first-child .menu-icon { + margin: 0; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a:first-child .menu-label { + opacity: 0; + pointer-events: none; + width: 0; + position: absolute; + overflow: hidden; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > .slide-menu { + top: auto; + min-width: 220px; + max-height: calc(100dvh - 16px); + padding: var(--fluent-space-xs); + background: var(--fluent-bg-card, var(--fluent-sidebar-bg)); + border: 1px solid var(--fluent-border); + border-radius: var(--fluent-radius-lg); + box-shadow: var(--fluent-shadow-lg); + z-index: calc(var(--fluent-zindex-menu) + 20); + margin: 0; + position: fixed; + inset-inline-start: calc(var(--fluent-sidebar-collapsed-width, 48px) + var(--fluent-space-sm)); + overflow: hidden auto; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > .slide-menu li a { + padding: 0 var(--fluent-space-sm); +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > .slide-menu.popup-open { + display: block; +} + +body[data-sidebar-state="collapsed"] .main-left .nav .menu:after { + display: none; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a.menu.popup-open { + background: var(--fluent-sidebar-item-active); +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a.menu.popup-open:before { + content: ""; + background: var(--fluent-primary); + border-radius: 999px; + width: 3px; + height: 60%; + position: absolute; + inset-inline-start: 3px; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a.menu.popup-open .menu-icon { + color: var(--fluent-primary); +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a.menu.popup-open .menu-icon:before { + opacity: 0; +} + +body[data-sidebar-state="collapsed"] .main-left .nav > li > a.menu.popup-open .menu-icon:after { + opacity: 1; +} + +@media (max-width: 767px) { + html[dir="ltr"] .main-left, body[data-fluent-dir="ltr"] .main-left { + transform: translateX(-100%); + } + + html[dir="ltr"] .main-left.active, body[data-fluent-dir="ltr"] .main-left.active { + transform: translateX(0); + } + + html[dir="rtl"] .main-left, body[data-fluent-dir="rtl"] .main-left { + transform: translateX(100%); + } + + html[dir="rtl"] .main-left.active, body[data-fluent-dir="rtl"] .main-left.active { + transform: translateX(0); + } + + .main-left { + z-index: calc(var(--fluent-zindex-modal) + 150); + height: 100dvh; + top: 0; + } + + .main-left .sidebar__search-slot { + padding-top: 16px; + display: block; + } + + .main-right, body[data-sidebar-state="collapsed"] .main-right { + margin-inline-start: 0; + } + + .darkMask { + top: 0; + -webkit-backdrop-filter: blur(2px); + backdrop-filter: blur(2px); + z-index: calc(var(--fluent-zindex-modal) + 149); + opacity: 0; + pointer-events: none; + transition: opacity var(--fluent-duration-normal) var(--fluent-easing-standard); + background-color: #0006; + position: fixed; + inset: 0; + } + + .darkMask.active { + opacity: 1; + pointer-events: auto; + } +} + +@keyframes fluent-poll-pulse { + 0% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: .3; + transform: scale(.8); + } + + 100% { + opacity: 1; + transform: scale(1); + } +} + +body > div.main > div.main-right > header { + align-items: center; + gap: var(--fluent-space-md); + padding: 0 var(--fluent-space-sm); + background: var(--fluent-bg); + border-bottom: 1px solid var(--fluent-border); + height: var(--fluent-header-height, 48px); + top: 0; + width: 100%; + z-index: var(--fluent-zindex-actions, 1000); + transition: background var(--fluent-duration-normal) var(--fluent-easing-standard); + display: flex; + position: fixed; + inset-inline-start: 0; +} + +body > div.main > div.main-right > header .header__brand { + align-items: center; + gap: var(--fluent-space-sm); + flex: none; + min-width: 0; + display: flex; +} + +body > div.main > div.main-right > header .header__title { + min-width: 0; + color: var(--fluent-text); + font-size: var(--fluent-font-size-md); + font-weight: var(--fluent-font-weight-semibold); + line-height: var(--fluent-line-height-tight); + white-space: nowrap; + text-overflow: ellipsis; + align-items: center; + text-decoration: none; + display: flex; + overflow: hidden; +} + +body > div.main > div.main-right > header .header__title .icon { + flex-shrink: 0; + width: 18px; + height: 18px; + margin-inline-end: 12px; +} + +body > div.main > div.main-right > header .header__search-slot { + flex: auto; + justify-content: center; + align-items: center; + min-width: 0; + display: flex; +} + +body > div.main > div.main-right > header .header__search-placeholder { + border-radius: var(--fluent-radius-md); + border: 1px solid var(--fluent-border); + background: var(--fluent-bg-hover); + width: min(420px, 100%); + height: 32px; + box-shadow: inset 0 1px #ffffff14; +} + +body > div.main > div.main-right > header .header__toggle, body > div.main > div.main-right > header .desktop-toggle { + width: 36px; + height: 36px; + color: var(--fluent-text); + cursor: pointer; + border-radius: var(--fluent-radius-md); + transition: background var(--fluent-duration-fast) var(--fluent-easing-standard); + background: none; + border: none; + justify-content: center; + align-items: center; + padding: 0; + display: flex; +} + +body > div.main > div.main-right > header .header__toggle:before, body > div.main > div.main-right > header .desktop-toggle:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-line-horizontal-3-20-regular: url("data:image/svg+xml,"); + width: 20px; + height: 20px; + -webkit-mask-image: var(--fluent-mask-line-horizontal-3-20-regular); + mask-image: var(--fluent-mask-line-horizontal-3-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body > div.main > div.main-right > header .header__toggle:hover, body > div.main > div.main-right > header .desktop-toggle:hover { + background: var(--fluent-bg-hover); +} + +body > div.main > div.main-right > header .mobile-toggle { + display: none !important; +} + +@media (max-width: 767px) { + body > div.main > div.main-right > header .mobile-toggle { + display: flex !important; + } +} + +body > div.main > div.main-right > header .desktop-toggle { + display: flex !important; +} + +@media (max-width: 767px) { + body > div.main > div.main-right > header .desktop-toggle { + display: none !important; + } +} + +body > div.main > div.main-right > header .theme-toggle { + box-sizing: border-box; + width: 32px; + height: 32px; + color: var(--fluent-text-secondary); + cursor: pointer; + border-radius: var(--fluent-radius-sm); + transition: all var(--fluent-transition-fast); + opacity: 0; + pointer-events: none; + background: none; + border: none; + flex: 0 0 32px; + justify-content: center; + align-items: center; + padding: 0; + display: inline-flex; + position: relative; + overflow: visible; +} + +body > div.main > div.main-right > header .theme-toggle.visible { + opacity: 1; + pointer-events: auto; +} + +body > div.main > div.main-right > header .theme-toggle:hover { + background: var(--fluent-bg-hover); + color: var(--fluent-text); +} + +body > div.main > div.main-right > header .theme-toggle:active { + transform: scale(.95); +} + +body > div.main > div.main-right > header .theme-toggle:before, body > div.main > div.main-right > header .theme-toggle:after { + pointer-events: none; + transition: opacity var(--fluent-transition-fast),transform var(--fluent-transition-fast),background-color var(--fluent-transition-fast); + position: absolute; +} + +body > div.main > div.main-right > header .theme-toggle:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-weather-sunny-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-weather-sunny-20-regular); + mask-image: var(--fluent-mask-weather-sunny-20-regular); + -webkit-user-select: none; + user-select: none; + backface-visibility: hidden; + opacity: 1; + display: inline-block; + top: 50%; + left: 50%; + transform: translate3d(-50%, -50%, 0) rotate(0); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body > div.main > div.main-right > header .theme-toggle:after { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-link-16-filled: url("data:image/svg+xml,"); + width: 12px; + height: 12px; + -webkit-mask-image: var(--fluent-mask-link-16-filled); + mask-image: var(--fluent-mask-link-16-filled); + -webkit-user-select: none; + user-select: none; + inset-inline-end: 2px; + opacity: 0; + display: inline-block; + bottom: 2px; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body > div.main > div.main-right > header .theme-toggle[data-active-theme="dark"]:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-weather-moon-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-weather-moon-20-regular); + mask-image: var(--fluent-mask-weather-moon-20-regular); + -webkit-user-select: none; + user-select: none; + display: inline-block; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: currentColor !important; +} + +body > div.main > div.main-right > header .theme-toggle[data-mode="auto"]:before { + -webkit-mask-image: var(--fluent-mask-weather-sunny-20-regular),radial-gradient(ellipse 6.5px 4.5px at 17px 17px, transparent 90%, black 100%); + mask-image: var(--fluent-mask-weather-sunny-20-regular),radial-gradient(ellipse 6.5px 4.5px at 17px 17px, transparent 90%, black 100%); + -webkit-mask-composite: source-in; + mask-composite: intersect; +} + +html[dir="rtl"] body > div.main > div.main-right > header .theme-toggle[data-mode="auto"]:before, body[data-fluent-dir="rtl"] body > div.main > div.main-right > header .theme-toggle[data-mode="auto"]:before { + -webkit-mask-image: var(--fluent-mask-weather-sunny-20-regular),radial-gradient(ellipse 6.5px 4.5px at 1px 17px, transparent 90%, black 100%); + mask-image: var(--fluent-mask-weather-sunny-20-regular),radial-gradient(ellipse 6.5px 4.5px at 1px 17px, transparent 90%, black 100%); +} + +body > div.main > div.main-right > header .theme-toggle[data-active-theme="dark"][data-mode="auto"]:before { + -webkit-mask-image: var(--fluent-mask-weather-moon-20-regular),radial-gradient(ellipse 6.5px 4.5px at 17px 17px, transparent 90%, black 100%); + mask-image: var(--fluent-mask-weather-moon-20-regular),radial-gradient(ellipse 6.5px 4.5px at 17px 17px, transparent 90%, black 100%); +} + +html[dir="rtl"] body > div.main > div.main-right > header .theme-toggle[data-active-theme="dark"][data-mode="auto"]:before, body[data-fluent-dir="rtl"] body > div.main > div.main-right > header .theme-toggle[data-active-theme="dark"][data-mode="auto"]:before { + -webkit-mask-image: var(--fluent-mask-weather-moon-20-regular),radial-gradient(ellipse 6.5px 4.5px at 1px 17px, transparent 90%, black 100%); + mask-image: var(--fluent-mask-weather-moon-20-regular),radial-gradient(ellipse 6.5px 4.5px at 1px 17px, transparent 90%, black 100%); +} + +body > div.main > div.main-right > header .theme-toggle[data-mode="auto"]:after { + opacity: 1; +} + +body > div.main > div.main-right > header .header-actions { + align-items: center; + gap: var(--fluent-space-sm); + flex: none; + display: flex; +} + +body > div.main > div.main-right > header .header-actions > * { + flex-shrink: 0; +} + +body > div.main > div.main-right > header .status { + align-items: center; + display: flex; +} + +body > div.main > div.main-right > header .status #xhr_poll_status, body > div.main > div.main-right > header .status [data-indicator="poll-status"], body > div.main > div.main-right > header .status [data-indicator="poll"] { + cursor: pointer; + border-radius: var(--fluent-radius-sm); + -webkit-user-select: none; + user-select: none; + transition: all var(--fluent-duration-fast) var(--fluent-easing-standard); + box-sizing: border-box; + justify-content: center; + align-items: center; + position: relative; + overflow: visible; + color: #0000 !important; + background-color: #0000 !important; + border: none !important; + width: 32px !important; + min-width: 32px !important; + height: 32px !important; + padding: 0 !important; + font-size: 0 !important; + display: inline-flex !important; +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="active"], body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="active"], body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="active"] { + --fluent-poll-icon: var(--fluent-text-secondary); +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="active"]:before, body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="active"]:before, body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="active"]:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-arrow-sync-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-arrow-sync-20-regular); + mask-image: var(--fluent-mask-arrow-sync-20-regular); + -webkit-user-select: none; + user-select: none; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + display: inline-block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-poll-icon) !important; +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="active"]:after, body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="active"]:after, body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="active"]:after { + content: ""; + inset-inline-end: 2px; + background-color: var(--fluent-success); + border: 2px solid var(--fluent-bg); + border-radius: 50%; + width: 8px; + height: 8px; + animation: 1.5s ease-in-out infinite fluent-poll-pulse; + position: absolute; + bottom: 2px; +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="active"]:hover, body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="active"]:hover, body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="active"]:hover { + --fluent-poll-icon: var(--fluent-text); + background-color: var(--fluent-bg-hover) !important; +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="inactive"], body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="inactive"], body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="inactive"] { + --fluent-poll-icon: var(--fluent-text-secondary); +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="inactive"]:before, body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="inactive"]:before, body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="inactive"]:before { + content: url("data:image/svg+xml;utf8,"); + --fluent-mask-arrow-sync-off-20-regular: url("data:image/svg+xml,"); + width: 18px; + height: 18px; + -webkit-mask-image: var(--fluent-mask-arrow-sync-off-20-regular); + mask-image: var(--fluent-mask-arrow-sync-off-20-regular); + -webkit-user-select: none; + user-select: none; + transition: background-color var(--fluent-duration-fast) var(--fluent-easing-standard); + display: inline-block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + background: var(--fluent-poll-icon) !important; +} + +body > div.main > div.main-right > header .status #xhr_poll_status[data-style="inactive"]:hover, body > div.main > div.main-right > header .status [data-indicator="poll-status"][data-style="inactive"]:hover, body > div.main > div.main-right > header .status [data-indicator="poll"][data-style="inactive"]:hover { + --fluent-poll-icon: var(--fluent-text); + background-color: var(--fluent-bg-hover) !important; +} + +body > div.main > div.main-right > header [data-theme="dark"] .theme-toggle, body > div.main > div.main-right > header .theme-toggle[data-active-theme="dark"] { + color: var(--fluent-text-secondary); +} + +body > div.main > div.main-right > header [data-theme="dark"] .theme-toggle:hover, body > div.main > div.main-right > header .theme-toggle[data-active-theme="dark"]:hover { + color: var(--fluent-text); + background: #ffffff14; +} + +@media (max-width: 767px) { + body > div.main > div.main-right > header { + padding: 0 var(--fluent-space-sm); + gap: var(--fluent-space-sm); + } + + body > div.main > div.main-right > header .header__brand { + flex: auto; + } + + body > div.main > div.main-right > header .header__search-slot { + display: none; + } + + body > div.main > div.main-right > header .theme-toggle { + flex: 0 0 28px; + width: 28px; + height: 28px; + } +} + +.main > .main-right > #maincontent { + box-sizing: border-box; + width: 100%; + padding: var(--fluent-space-xl); + flex-grow: 1; + overflow: auto; +} + +.main > .main-right > #maincontent > .container > h2[name="content"] { + margin-bottom: 15px; +} + +.main > .main-right > #maincontent > .container > h2, .main > .main-right > #maincontent > .container #view > h2, .main > .main-right > #maincontent > .container #view > div > h2 { + padding-inline-start: 12px; + position: relative; +} + +.main > .main-right > #maincontent > .container > h2:before, .main > .main-right > #maincontent > .container #view > h2:before, .main > .main-right > #maincontent > .container #view > div > h2:before { + content: ""; + inset-inline-start: 0; + background-color: var(--fluent-primary); + border-radius: var(--fluent-radius-sm, 2px); + width: 4px; + position: absolute; + top: 4px; + bottom: 4px; +} + +@media (max-width: 767px) { + .main > .main-right > #maincontent { + padding: var(--fluent-space-md); + } +} + +[data-theme="dark"] { + --lightningcss-light: ; + --lightningcss-dark: initial; + color-scheme: dark; + --fluent-bg: #1f1f1f; + --fluent-bg-card: #292929; + --fluent-bg-hover: #383838; + --fluent-bg-active: #424242; + --fluent-bg-page: #1a1a1a; + --fluent-primary: #4da6ff; + --fluent-primary-hover: var(--fluent-brand-90); + --fluent-primary-active: var(--fluent-brand-100); + --fluent-primary-light: color-mix(in srgb, var(--fluent-primary) 15%, #000); + --fluent-primary-lighter: color-mix(in srgb, var(--fluent-primary) 8%, #000); + --fluent-primary-lightest: color-mix(in srgb, var(--fluent-primary) 12%, #000); + --fluent-selection-bg: color-mix(in srgb, var(--fluent-primary) 35%, #000); + --fluent-selection-text: var(--fluent-text); + --fluent-info: var(--fluent-primary); + --fluent-info-light: var(--fluent-primary-light); + --fluent-success: #6feb6f; + --fluent-success-light: #133c13; + --fluent-warning: #ff8c00; + --fluent-warning-hover: #fa3; + --fluent-warning-active: #fb5; + --fluent-warning-light: #420; + --fluent-error: #ff5f5f; + --fluent-error-hover: #ff8080; + --fluent-error-active: #ff9f9f; + --fluent-error-light: #400; + --fluent-purple: #c084fc; + --fluent-purple-light: #c084fc2e; + --fluent-text: #fff; + --fluent-text-secondary: #adadad; + --fluent-text-disabled: #6b6b6b; + --fluent-text-on-primary: #fff; + --fluent-text-link: var(--fluent-primary); + --fluent-text-link-hover: var(--fluent-primary-hover); + --fluent-border: #525252; + --fluent-border-hover: #7a7a7a; + --fluent-border-active: var(--fluent-primary); + --fluent-shadow-sm: 0 2px 4px #0003, 0 2px 10px #0000004d; + --fluent-shadow-md: 0 4px 8px #00000040, 0 4px 18px #00000059; + --fluent-shadow-lg: 0 8px 16px #0000004d, 0 8px 32px #0006; + --fluent-shadow-xl: 0 20px 40px #0006, 0 10px 20px #0000004d; + --fluent-sidebar-bg: #1f1f1f; + --fluent-sidebar-item-hover: #ffffff0f; + --fluent-sidebar-item-active: color-mix(in srgb, var(--fluent-primary) 8%, transparent); + --fluent-input-bg: #292929; + --fluent-input-border: #666; + --fluent-input-border-bottom: #adadad; + --fluent-input-border-hover: #757575; + --fluent-input-border-bottom-hover: #bdbdbd; + --fluent-input-border-bottom-focus: var(--fluent-primary); + --fluent-input-bg-disabled: #333; + --fluent-input-bg-hover: #333; + --fluent-calendar-picker-invert: 1; + --fluent-btn-primary-bg: color-mix(in srgb, var(--fluent-primary) 65%, #000); + --fluent-btn-primary-bg-hover: color-mix(in srgb, var(--fluent-primary) 75%, #000); + --fluent-btn-primary-bg-active: color-mix(in srgb, var(--fluent-primary) 50%, #000); + --fluent-btn-secondary-bg: #292929; + --fluent-btn-secondary-bg-hover: #3d3d3d; + --fluent-btn-secondary-bg-active: #1f1f1f; + --fluent-btn-secondary-border: #666; + --fluent-btn-secondary-border-hover: #757575; + --fluent-btn-secondary-border-active: #757575; + --fluent-card-bg: #292929; + --fluent-card-border: #3d3d3d; + --fluent-table-header-bg: #292929; + --fluent-table-row-hover: #383838; + --fluent-table-row-selected: color-mix(in srgb, var(--fluent-primary) 25%, #000); + --fluent-table-border: #3d3d3d; + --fluent-modal-bg: #292929; + --fluent-modal-overlay: #0009; + --fluent-checkbox-border: #adadad; + --fluent-checkbox-bg: #292929; + --fluent-checkbox-checked-bg: var(--fluent-primary); + --fluent-checkbox-checked-border: var(--fluent-primary); + --fluent-checkbox-hover-border: #fff; + --fluent-scrollbar-thumb-bg: #525252; + --fluent-scrollbar-thumb-bg-hover: #7a7a7a; + --fluent-scrollbar-thumb-horizontal-bg: #ffffff26; + --fluent-scrollbar-thumb-horizontal-bg-hover: #ffffff59; + --fluent-progress-bg: #3d3d3d; + --fluent-progress-bar-bg: var(--fluent-primary); + --fluent-login-bg: #1f1f1feb; + --fluent-login-border: #525252b3; + --fluent-login-icon-color: var(--fluent-text-secondary); + --fluent-tooltip-bg: #292929; + --fluent-tooltip-text: #fff; + --fluent-dropdown-bg: var(--fluent-bg-card); + --fluent-dropdown-border: var(--fluent-input-border); + --fluent-dropdown-shadow: 0 8px 16px #00000047, 0 0 2px #0000003d; + --fluent-dropdown-option-hover: var(--fluent-bg-hover); + --fluent-dropdown-option-selected: #4da6ff1a; + --fluent-focus-color: #fff; + --fluent-focus-inner-color: #000; +} + diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/favicon.ico b/luci-theme-fluent/htdocs/luci-static/fluent/favicon.ico new file mode 100644 index 000000000..21b9367fd Binary files /dev/null and b/luci-theme-fluent/htdocs/luci-static/fluent/favicon.ico differ diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/icon/favicon-32.png b/luci-theme-fluent/htdocs/luci-static/fluent/icon/favicon-32.png new file mode 100644 index 000000000..21b9367fd Binary files /dev/null and b/luci-theme-fluent/htdocs/luci-static/fluent/icon/favicon-32.png differ diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/icon/favicon.ico b/luci-theme-fluent/htdocs/luci-static/fluent/icon/favicon.ico new file mode 100644 index 000000000..21b9367fd Binary files /dev/null and b/luci-theme-fluent/htdocs/luci-static/fluent/icon/favicon.ico differ diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/icon/icon-192.png b/luci-theme-fluent/htdocs/luci-static/fluent/icon/icon-192.png new file mode 100644 index 000000000..782c5c4c7 Binary files /dev/null and b/luci-theme-fluent/htdocs/luci-static/fluent/icon/icon-192.png differ diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/icon/manifest.json b/luci-theme-fluent/htdocs/luci-static/fluent/icon/manifest.json new file mode 100644 index 000000000..e7b48d7b5 --- /dev/null +++ b/luci-theme-fluent/htdocs/luci-static/fluent/icon/manifest.json @@ -0,0 +1,10 @@ +{ + "name": "OpenWrt", + "icons": [ + { + "src": "/luci-static/fluent/icon/icon-192.png", + "sizes": "192x192", + "type": "image/png" + } + ] +} diff --git a/luci-theme-fluent/htdocs/luci-static/fluent/img/fluent.svg b/luci-theme-fluent/htdocs/luci-static/fluent/img/fluent.svg new file mode 100644 index 000000000..c488a204f --- /dev/null +++ b/luci-theme-fluent/htdocs/luci-static/fluent/img/fluent.svg @@ -0,0 +1,100 @@ + + Fluent + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/luci-theme-fluent/htdocs/luci-static/resources/menu-fluent.js b/luci-theme-fluent/htdocs/luci-static/resources/menu-fluent.js new file mode 100644 index 000000000..a7e328574 --- /dev/null +++ b/luci-theme-fluent/htdocs/luci-static/resources/menu-fluent.js @@ -0,0 +1,2665 @@ +"use strict"; +"require baseclass"; +"require ui"; +"require dom"; +// WARN: generated by rsbuild - do not edit + +// The require scope +var __webpack_require__ = {}; + +// webpack/runtime/define_property_getters +(() => { +__webpack_require__.d = (exports, getters, values) => { + var define = (defs, kind) => { + for(var key in defs) { + if(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] }); + } + } + }; + define(getters, "get"); + define(values, "value"); +}; +})(); +// webpack/runtime/has_own_property +(() => { +__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +})(); +// webpack/runtime/make_namespace_object +(() => { +// define __esModule on exports +__webpack_require__.r = (exports) => { + if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { + Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + } + Object.defineProperty(exports, '__esModule', { value: true }); +}; +})(); +var __webpack_exports__ = {}; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + main: () => (/* binding */ main) +}); + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-factory.ts +const Fragment = Symbol.for("jsx.fragment"); +function jsx_factory_e(e, t) { + let { children: n, ...r } = null === t || "object" != typeof t || Array.isArray(t) ? {} : t, o = function e(t, n = []) { + for (let r of t)null != r && "boolean" != typeof r && (Array.isArray(r) ? e(r, n) : n.push(r instanceof Node ? r : String(r))); + return n; + }(null == n ? [] : Array.isArray(n) ? n : [ + n + ]); + if (e === Fragment) { + let e = document.createDocumentFragment(); + return e.append(...o), e; + } + if ("function" == typeof e) { + let t = Reflect.apply(e, void 0, [ + { + ...r, + children: o + } + ]); + if (!(t instanceof Node)) throw TypeError("JSX components must return a DOM Node"); + return t; + } + if ("string" != typeof e) throw TypeError("JSX element types must be tag names or component functions"); + let f = {}, l = { + ...r + }; + for (let [e, t] of Object.entries(l))e.startsWith("on") && "function" == typeof t ? (f[e] = t, delete l[e]) : "boolean" == typeof t && (t ? l[e] = e : delete l[e]); + let s = Object.keys(l).length > 0 ? o.length > 1 ? E(e, l, o) : E(e, l, o[0]) : o.length > 1 ? E(e, {}, o) : E(e, {}, o[0]); + for (let [e, t] of Object.entries(f)){ + let n = e.slice(2).toLowerCase(); + s.addEventListener(n, t); + } + return s; +} +function jsx(t, n) { + return jsx_factory_e(t, n); +} +function jsxs(t, n) { + return jsx_factory_e(t, n); +} +function jsxDEV(t, n) { + return jsx_factory_e(t, n); +} + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-runtime.ts + + +;// CONCATENATED MODULE: ./web/resources/menu-layout.ts +let menu_layout_e = "primary:", menu_layout_t = new Set([ + "version", + "custom", + "titles", + "itemTitles", + "categoryMoves", + "itemMoves", + "hiddenCategories", + "hiddenItems" +]); +function menu_layout_n(e) { + return "function" == typeof _ ? _(e) : e; +} +function primaryCategoryId(t) { + return `${menu_layout_e}${t}`; +} +function menu_layout_i(t) { + return t.startsWith(menu_layout_e) ? t.slice(menu_layout_e.length) : null; +} +function menu_layout_r(e) { + let t = [], i = []; + for (let r of ui.menu.getChildren(e))if (r.satisfied) for (let e of ui.menu.getChildren(r)){ + let l = e.title?.trim(); + if (!e.satisfied || !l) continue; + let o = `${r.name}/${e.name}`, u = menu_layout_n(l); + for (let a of (t.push({ + path: o, + pathSegments: [ + r.name, + e.name + ], + node: e, + title: u, + rawTitle: l + }), ui.menu.getChildren(e))){ + let t = a.title?.trim(); + a.satisfied && t && i.push({ + path: `${o}/${a.name}`, + pathSegments: [ + r.name, + e.name, + a.name + ], + node: a, + title: menu_layout_n(t), + rawTitle: t, + originalPrimaryPath: o, + originalPrimaryTitle: u + }); + } + } + return { + primaries: t, + items: i + }; +} +function discoverMenuItems(e) { + return menu_layout_r(e).items; +} +function menu_layout_l(e) { + let t = e.primaries.map((e)=>({ + id: primaryCategoryId(e.path), + title: e.title, + items: [] + })), n = new Map(t.map((e)=>[ + e.id, + e + ])); + for (let t of e.items)n.get(primaryCategoryId(t.originalPrimaryPath))?.items.push(t.path); + return t; +} +function buildDefaultMenuCategories(e) { + return menu_layout_l(menu_layout_r(e)); +} +function menu_layout_o(e) { + return e.map((e)=>({ + ...e, + items: [ + ...e.items + ] + })); +} +function moveMenuItem(e, t, n, i) { + let r = menu_layout_o(e), l = r.find((e)=>e.id === n); + if (!l || i === t) return r; + for (let e of r)e.items = e.items.filter((e)=>e !== t); + let u = i ? l.items.indexOf(i) : -1; + return u >= 0 ? l.items.splice(u, 0, t) : l.items.push(t), r; +} +function moveMenuCategory(e, t, n, i = "before") { + let r = menu_layout_o(e), l = r.findIndex((e)=>e.id === t); + if (l < 0 || t === n || !r.some((e)=>e.id === n)) return r; + let [u] = r.splice(l, 1), a = r.findIndex((e)=>e.id === n); + return r.splice("after" === i ? a + 1 : a, 0, u), r; +} +function menu_layout_u(e, t) { + let n = e[t]; + return new Set(e.slice(t + 1).filter((e)=>e.originalPrimaryPath === n.originalPrimaryPath).map((e)=>e.path)); +} +function canRestoreMenuItem(e, t, n) { + let i = t.findIndex((e)=>e.path === n); + if (i < 0) return !1; + let r = primaryCategoryId(t[i].originalPrimaryPath), l = e.find((e)=>e.id === r); + if (!l) return !0; + let o = 0, a = -1; + for (let t of e)for(let e = 0; e < t.items.length; e += 1)t.items[e] === n && (o += 1, t.id === r && (a = e)); + if (1 !== o || a < 0) return !0; + let s = menu_layout_u(t, i), f = l.items.findIndex((e)=>s.has(e)); + return (f < 0 ? l.items.length - 1 : f > a ? f - 1 : f) !== a; +} +function restoreMenuItemToOriginalPosition(e, t, n) { + let i = t.findIndex((e)=>e.path === n); + if (i < 0) return menu_layout_o(e); + let r = t[i], l = primaryCategoryId(r.originalPrimaryPath), a = e.some((e)=>e.id === l) ? e : [ + ...e, + { + id: l, + title: r.originalPrimaryTitle, + items: [] + } + ], s = menu_layout_u(t, i), f = a.find((e)=>e.id === l); + return moveMenuItem(a, n, l, f?.items.find((e)=>s.has(e))); +} +function menu_layout_a(e, t) { + if ("string" != typeof e || e !== e.trim()) return !1; + let n = e.split("/"); + return n.length === t && n.every(Boolean); +} +function menu_layout_s(e) { + return "number" == typeof e ? `custom:${e}` : `primary:${e}`; +} +function menu_layout_f(e, t) { + return Number.isInteger(e) && e >= 0 && e < t || menu_layout_a(e, 2) ? e : null; +} +function menu_layout_d(e, t) { + let n = e[t]; + return void 0 === n ? [] : Array.isArray(n) ? n : null; +} +function parseMenuLayout(e) { + let n; + if ("string" != typeof e || "" === e.trim()) return null; + try { + n = JSON.parse(e); + } catch { + return null; + } + if (!n || "object" != typeof n || Array.isArray(n)) return null; + let i = n; + if (1 !== i.version || Object.keys(i).some((e)=>!menu_layout_t.has(e))) return null; + let r = menu_layout_d(i, "custom"), l = menu_layout_d(i, "titles"), o = menu_layout_d(i, "itemTitles"), u = menu_layout_d(i, "categoryMoves"), m = menu_layout_d(i, "itemMoves"), p = menu_layout_d(i, "hiddenCategories"), h = menu_layout_d(i, "hiddenItems"); + if (!r || !l || !o || !u || !m || !p || !h) return null; + let g = [], y = new Set(); + for (let e of r){ + if ("string" != typeof e) return null; + let t = e.trim(), n = t.toLocaleLowerCase(); + if (!t || y.has(n)) return null; + y.add(n), g.push(t); + } + let c = [], M = new Set(); + for (let e of l){ + if (!Array.isArray(e) || 2 !== e.length || !menu_layout_a(e[0], 2) || "string" != typeof e[1]) return null; + let t = e[1].trim(); + if (!t || M.has(e[0])) return null; + M.add(e[0]), c.push([ + e[0], + t + ]); + } + let v = [], w = new Set(); + for (let e of o){ + if (!Array.isArray(e) || 2 !== e.length || !menu_layout_a(e[0], 3) || "string" != typeof e[1]) return null; + let t = e[1].trim(); + if (!t || w.has(e[0])) return null; + w.add(e[0]), v.push([ + e[0], + t + ]); + } + let I = [], C = new Set(); + for (let e of u){ + if (!Array.isArray(e) || 2 !== e.length) return null; + let t = menu_layout_f(e[0], g.length), n = null === e[1] ? null : menu_layout_f(e[1], g.length); + if (null === t || null !== e[1] && null === n) return null; + let i = menu_layout_s(t); + if (C.has(i) || null !== n && i === menu_layout_s(n)) return null; + C.add(i), I.push([ + t, + n + ]); + } + let S = [], x = new Set(); + for (let e of m){ + if (!Array.isArray(e) || 3 !== e.length || !menu_layout_a(e[0], 3)) return null; + let t = menu_layout_f(e[1], g.length), n = null === e[2] ? null : menu_layout_a(e[2], 3) ? e[2] : null; + if (null === t || null !== e[2] && null === n || x.has(e[0]) || n === e[0]) return null; + x.add(e[0]), S.push([ + e[0], + t, + n + ]); + } + let P = [], A = new Set(); + for (let e of p){ + let t = menu_layout_f(e, g.length); + if (null === t || A.has(menu_layout_s(t))) return null; + A.add(menu_layout_s(t)), P.push(t); + } + let T = [], b = new Set(); + for (let e of h){ + if (!menu_layout_a(e, 3) || b.has(e)) return null; + b.add(e), T.push(e); + } + return { + version: 1, + custom: g, + titles: c, + itemTitles: v, + categoryMoves: I, + itemMoves: S, + hiddenCategories: P, + hiddenItems: T + }; +} +function menu_layout_m() { + return { + titles: [], + itemTitles: [], + categoryMoves: [], + itemMoves: [] + }; +} +function menu_layout_p(e, t) { + return "number" == typeof e ? t[e] ?? null : primaryCategoryId(e); +} +function resolveMenuLayout(e, t) { + let n = menu_layout_r(e), i = menu_layout_l(n), u = new Set(), a = new Set(), s = menu_layout_m(); + if (!t) return { + categories: i, + hiddenCategoryIds: u, + hiddenItemPaths: a, + itemTitles: new Map(), + pending: s, + configured: !1 + }; + let f = t.custom.map((e, t)=>{ + let n = `custom:${t}`; + return i.push({ + id: n, + title: e, + items: [] + }), n; + }), d = new Set(n.items.map((e)=>e.path)), h = f.length > 0; + for (let [e, n] of t.titles){ + let t = primaryCategoryId(e), r = i.find((e)=>e.id === t); + r ? (r.title = n, h = !0) : s.titles.push([ + t, + n + ]); + } + let g = new Map(); + for (let [e, n] of t.itemTitles)d.has(e) ? (g.set(e, n), h = !0) : s.itemTitles.push([ + e, + n + ]); + for (let [e, n] of t.categoryMoves){ + let t = menu_layout_p(e, f), r = null === n ? null : menu_layout_p(n, f), l = null !== t && i.some((e)=>e.id === t), u = null === r || i.some((e)=>e.id === r); + t && l && u ? (i = function(e, t, n) { + if (n) return moveMenuCategory(e, t, n); + let i = menu_layout_o(e), r = i.findIndex((e)=>e.id === t); + if (r < 0) return i; + let [l] = i.splice(r, 1); + return i.push(l), i; + }(i, t, r), h = !0) : t && s.categoryMoves.push([ + t, + r + ]); + } + for (let [e, n, r] of t.itemMoves){ + let t = menu_layout_p(n, f), l = t ? i.find((e)=>e.id === t) : void 0, o = null === r || !!l?.items.includes(r); + d.has(e) && t && l && o ? (i = moveMenuItem(i, e, t, r ?? void 0), h = !0) : t && s.itemMoves.push([ + e, + t, + r + ]); + } + for (let e of t.hiddenCategories){ + let t = menu_layout_p(e, f); + t && (u.add(t), i.some((e)=>e.id === t) && (h = !0)); + } + for (let e of t.hiddenItems)a.add(e), d.has(e) && (h = !0); + return { + categories: i, + hiddenCategoryIds: u, + hiddenItemPaths: a, + itemTitles: g, + pending: s, + configured: h + }; +} +function menu_layout_h(e, t) { + let n = new Map(t.map((e, t)=>[ + e, + t + ])), i = e.flatMap((e)=>{ + let t = n.get(e); + return void 0 === t ? [] : [ + { + value: e, + index: t + } + ]; + }), r = [], l = Array(i.length).fill(-1); + for(let e = 0; e < i.length; e += 1){ + let t = 0, n = r.length; + for(; t < n;){ + let l = t + n >> 1; + i[r[l]].index < i[e].index ? t = l + 1 : n = l; + } + t > 0 && (l[e] = r[t - 1]), r[t] = e; + } + let o = new Set(); + for(let e = r.at(-1) ?? -1; e >= 0; e = l[e])o.add(i[e].value); + return o; +} +function menu_layout_g(e, t) { + let n = menu_layout_i(e); + return n || (t.get(e) ?? null); +} +function menu_layout_y(e, t, n) { + let i = n.get(e), r = n.get(t); + return void 0 !== i && void 0 !== r ? i - r : void 0 !== i ? -1 : void 0 !== r ? 1 : e.localeCompare(t); +} +function serializeMenuLayout(e, t, n, o, u = menu_layout_m(), a = new Map()) { + let s = menu_layout_r(e), f = menu_layout_l(s), d = new Map(f.map((e)=>[ + e.id, + e + ])), p = t.filter((e)=>!d.has(e.id)), c = new Map(p.map((e, t)=>[ + e.id, + t + ])), M = p.map((e)=>e.title.trim()), v = [], w = new Set(); + for (let e of t){ + let t = d.get(e.id), n = e.title.trim(); + if (!t || n === t.title) continue; + let r = menu_layout_i(e.id); + r && (v.push([ + r, + n + ]), w.add(e.id)); + } + for (let [e, t] of u.titles){ + if (w.has(e)) continue; + let n = menu_layout_i(e); + n && v.push([ + n, + t + ]); + } + let I = new Map(s.items.map((e)=>[ + e.path, + e.title + ])), C = [], S = new Set(); + for (let [e, t] of a){ + let n = t.trim(); + n && n !== I.get(e) && (C.push([ + e, + n + ]), S.add(e)); + } + for (let [e, t] of u.itemTitles)S.has(e) || C.push([ + e, + t + ]); + let x = [ + ...f.map((e)=>e.id), + ...p.map((e)=>e.id) + ], P = t.map((e)=>e.id), A = [], T = new Set(); + for (let [e, t] of function(e, t) { + let n = menu_layout_h(e, t), i = []; + for(let e = t.length - 1; e >= 0; e -= 1)n.has(t[e]) || i.push([ + t[e], + t[e + 1] ?? null + ]); + return i; + }(x, P)){ + let n = menu_layout_g(e, c), i = null === t ? null : menu_layout_g(t, c); + null !== n && (null === t || null !== i) && (A.push([ + n, + i + ]), T.add(e)); + } + for (let [e, t] of u.categoryMoves){ + if (T.has(e)) continue; + let n = menu_layout_g(e, c), i = null === t ? null : menu_layout_g(t, c); + null !== n && (null === t || null !== i) && A.push([ + n, + i + ]); + } + let b = [], $ = new Set(); + for (let e of t){ + let t = menu_layout_g(e.id, c); + if (null === t) continue; + let n = menu_layout_h(d.get(e.id)?.items ?? [], e.items); + for(let i = e.items.length - 1; i >= 0; i -= 1){ + let r = e.items[i]; + n.has(r) || (b.push([ + r, + t, + e.items[i + 1] ?? null + ]), $.add(r)); + } + } + for (let [e, t, n] of u.itemMoves){ + if ($.has(e)) continue; + let i = menu_layout_g(t, c); + null !== i && b.push([ + e, + i, + n + ]); + } + let L = new Map(t.map((e, t)=>[ + e.id, + t + ])), O = [ + ...n + ].sort((e, t)=>menu_layout_y(e, t, L)).flatMap((e)=>{ + let t = menu_layout_g(e, c); + return null === t ? [] : [ + t + ]; + }), j = new Map(s.items.map((e, t)=>[ + e.path, + t + ])), N = [ + ...o + ].sort((e, t)=>menu_layout_y(e, t, j)), k = { + version: 1 + }; + return M.length > 0 && (k.custom = M), v.length > 0 && (k.titles = v), C.length > 0 && (k.itemTitles = C), A.length > 0 && (k.categoryMoves = A), b.length > 0 && (k.itemMoves = b), O.length > 0 && (k.hiddenCategories = O), N.length > 0 && (k.hiddenItems = N), 1 === Object.keys(k).length ? "" : JSON.stringify(k); +} +function buildMenuPresentation(e, t) { + let n = menu_layout_r(e), l = resolveMenuLayout(e, parseMenuLayout(t)), o = new Map(n.items.map((e)=>[ + e.path, + e + ])); + for (let [e, t] of l.itemTitles){ + let n = o.get(e); + n && o.set(e, { + ...n, + title: t + }); + } + let u = new Map(n.primaries.map((e)=>[ + primaryCategoryId(e.path), + e + ])), a = new Set(l.hiddenItemPaths); + for (let e of l.hiddenCategoryIds){ + let t = menu_layout_i(e); + t && a.add(t); + let n = l.categories.find((t)=>t.id === e); + if (n) for (let e of n.items)a.add(e); + } + return { + categories: l.categories.map((e)=>({ + id: e.id, + title: e.title, + primary: u.get(e.id) ?? null, + items: e.items.flatMap((e)=>{ + let t = o.get(e); + return t ? [ + t + ] : []; + }) + })), + hiddenCategoryIds: l.hiddenCategoryIds, + hiddenPaths: a, + configured: l.configured + }; +} + +;// CONCATENATED MODULE: ./web/resources/utils/menu-cache.ts +let menu_cache_e = "fluent_menu_cache"; +function menu_cache_t(e) { + let t = document.body?.getAttribute("data-menu-cache-scope"); + return t ? `${e}_${t}` : null; +} +function getCachedMenu() { + try { + let n = menu_cache_t(menu_cache_e); + if (!n) return null; + let r = sessionStorage.getItem(n); + if (!r) return null; + let u = JSON.parse(r); + if (u && "object" == typeof u) return { + tree: u, + raw: r + }; + } catch (e) {} + return null; +} +function saveMenuCache(n) { + try { + let r = menu_cache_t(menu_cache_e); + if (!r) return null; + let u = JSON.stringify(n); + return sessionStorage.setItem(r, u), u; + } catch (e) { + return null; + } +} +function saveRenderedHtmlCache() { + try { + let e = document.querySelector("#mainmenu"), n = menu_cache_t("fluent_sidebar_html"); + e && n && sessionStorage.setItem(n, e.innerHTML); + let r = document.querySelector("#tabmenu"), u = menu_cache_t("fluent_tabmenu_html"), l = Array.isArray(L?.env?.dispatchpath) ? JSON.stringify(L.env.dispatchpath) : ""; + r && r.children.length > 0 && u && l && sessionStorage.setItem(`${u}_${l}`, r.innerHTML); + } catch (e) {} +} +function getResolvedMenuLayout() { + let e = document.body?.getAttribute?.("data-menu-layout"); + if (e) try { + let t = JSON.parse(e); + if ("string" == typeof t || Array.isArray(t) || null === t) return t; + } catch (e) {} +} + +;// CONCATENATED MODULE: ./web/resources/utils/apply-change-preview.ts +let apply_change_preview_e = Symbol("fluentApplyChangePreview"); +function apply_change_preview_t(e, t) { + e.appendChild(document.createTextNode(t)); +} +function apply_change_preview_n(e, t) { + let n = document.createElement("strong"); + n.textContent = t, e.appendChild(n); +} +function apply_change_preview_l(e) { + return `'${e.replaceAll("'", "'\"'\"'")}'`; +} +function apply_change_preview_r(e) { + try { + return JSON.stringify(e) ?? String(e); + } catch { + return "[unserializable UCI change record]"; + } +} +function apply_change_preview_i(e) { + return null === e || "object" != typeof e ? [] : Object.entries(e).flatMap(([e, t])=>Array.isArray(t) && t.length ? [ + [ + e, + t + ] + ] : []); +} +function apply_change_preview_u(e, t) { + let n = document.createElement(e); + return n.textContent = t, n; +} +function apply_change_preview_a(e, l, r) { + let i = document.createElement("var"), u = document.createElement(e); + return apply_change_preview_t(u, l), apply_change_preview_n(u, r), i.appendChild(u), i; +} +function setupApplyChangePreview() { + let c = ui.changes; + if (c[apply_change_preview_e] || "function" != typeof c.apply || "function" != typeof c.displayStatus) return; + let o = c.apply.bind(c), d = c.displayStatus.bind(c), p = 0, s = null, m = ()=>{ + let e = s; + if (null === e || null === e.changes || e.dismissed) return; + let i = document.querySelector("#modal_overlay .modal.alert-message.notice.spinning"); + if (!i) return; + let c = i.parentElement || i, o = c.querySelectorAll(".fluent-apply-change-preview"); + if (1 === o.length && o[0] === e.preview) return; + o.forEach((e)=>{ + e.remove(); + }); + let d = e.preview || function(e, i) { + let c = document.createElement("div"); + for (let [o, d] of (c.className = "fluent-apply-change-preview", e)){ + let e = document.createElement("div"); + e.className = "fluent-apply-change-preview-card"; + let p = document.createElement("div"); + p.className = "fluent-apply-change-preview-card-header"; + let s = document.createElement("span"); + s.textContent = `# /etc/config/${o}`, p.appendChild(s); + let m = document.createElement("button"); + m.className = "fluent-apply-change-preview-close", m.type = "button", m.title = "Close preview", m.onclick = ()=>{ + i && i(), c.remove(); + }, p.appendChild(m), e.appendChild(p); + let v = document.createElement("div"); + v.className = "fluent-apply-change-preview-card-body uci-change-list"; + let f = null; + for (let e of d)v.appendChild(function(e, i, c) { + if (!Array.isArray(i) || !i.every((e)=>"string" == typeof e)) return apply_change_preview_u("var", apply_change_preview_r(i)); + let [o, d, p, s] = i, m = void 0 === d ? void 0 : null !== c && d === c[0] ? `@${c[1]}[-1]` : d; + if ("add" === o && 3 === i.length && void 0 !== m && void 0 !== p) { + let l = document.createElement("ins"); + return apply_change_preview_t(l, `uci add ${e} `), apply_change_preview_n(l, p), apply_change_preview_t(l, ` # =${m}`), l; + } + if ("set" === o && 3 === i.length && void 0 !== m && void 0 !== p) { + let l = document.createElement("ins"); + return apply_change_preview_t(l, `uci set ${e}.`), apply_change_preview_n(l, m), apply_change_preview_t(l, `=${p}`), l; + } + if ("set" === o && 4 === i.length && void 0 !== m && void 0 !== p && void 0 !== s) return apply_change_preview_a("ins", `uci set ${e}.${m}.${p}=`, apply_change_preview_l(s)); + if ("remove" === o && 2 === i.length && void 0 !== m) { + let l = document.createElement("del"); + return apply_change_preview_t(l, `uci del ${e}.`), apply_change_preview_n(l, m), l; + } + if ("remove" === o && 3 === i.length && void 0 !== m && void 0 !== p) return apply_change_preview_a("del", `uci del ${e}.${m}.`, p); + if ("order" === o && 3 === i.length && void 0 !== m && void 0 !== p) { + let l = document.createElement("var"); + return apply_change_preview_t(l, `uci reorder ${e}.${m}=`), apply_change_preview_n(l, p), l; + } + if ("list-add" === o && 4 === i.length && void 0 !== m && void 0 !== p && void 0 !== s) return apply_change_preview_a("ins", `uci add_list ${e}.${m}.${p}=`, apply_change_preview_l(s)); + if ("list-del" === o && 4 === i.length && void 0 !== m && void 0 !== p && void 0 !== s) return apply_change_preview_a("del", `uci del_list ${e}.${m}.${p}=`, apply_change_preview_l(s)); + if ("rename" === o && 3 === i.length && void 0 !== m && void 0 !== p) { + let l = document.createElement("var"); + return apply_change_preview_t(l, `uci rename ${e}.${m}=`), apply_change_preview_n(l, p), l; + } + if ("rename" === o && 4 === i.length && void 0 !== m && void 0 !== p && void 0 !== s) { + let r = document.createElement("var"); + return apply_change_preview_t(r, `uci rename ${e}.${m}.${p}=`), apply_change_preview_n(r, apply_change_preview_l(s)), r; + } + return apply_change_preview_u("var", apply_change_preview_r(i)); + }(o, e, f)), Array.isArray(e) && 3 === e.length && "add" === e[0] && "string" == typeof e[1] && "string" == typeof e[2] && (f = [ + e[1], + e[2] + ]); + e.appendChild(v), c.appendChild(e); + } + return c; + }(e.changes, ()=>{ + e.dismissed = !0; + }); + c.appendChild(d), e.preview = d; + }, v = null; + c.displayStatus = (e, t)=>{ + if ("notice spinning" !== e ? null === v && (v = setTimeout(()=>{ + "function" == typeof document.querySelectorAll && document.querySelectorAll(".fluent-apply-change-preview").forEach((e)=>{ + e.remove(); + }), v = null; + }, 150)) : null !== v && (clearTimeout(v), v = null), d(e, t), !e) { + p += 1, s = null; + return; + } + "notice spinning" === e && m(); + }, "u" > typeof MutationObserver && new MutationObserver(m).observe(document, { + childList: !0, + subtree: !0 + }), c.apply = (e)=>{ + let t = ++p; + s = null; + let n = L.uci.get_first("fluent", "global", "uci_changes_preview"); + if ("1" === n || null == n) { + let e = apply_change_preview_i(c.changes); + s = { + token: t, + changes: e.length ? e : null, + preview: null + }; + try { + L.uci.changes().then((e)=>{ + if (s?.token !== t) return; + let n = apply_change_preview_i(e); + n.length && (s.changes = n, m()); + }, ()=>{ + s?.token === t && null === s.changes && (s = null); + }); + } catch { + s?.token === t && null === s.changes && (s = null); + } + } + o(e); + }, c[apply_change_preview_e] = !0; +} + +;// CONCATENATED MODULE: ./web/resources/utils/error-tooltips.tsx + +function setupErrorTooltips() { + document.querySelectorAll(".cbi-input-invalid").forEach((t)=>{ + error_tooltips_i(t); + }), new MutationObserver((t)=>{ + t.forEach((t)=>{ + if ("attributes" === t.type) { + let r, n = t.target; + "class" === t.attributeName ? n.classList.contains("cbi-input-invalid") ? error_tooltips_i(n) : (r = error_tooltips_e(n).nextElementSibling, r?.classList.contains("fluent-error-message") && r.remove()) : "data-tooltip" === t.attributeName && n.classList.contains("cbi-input-invalid") && error_tooltips_i(n); + } else "childList" === t.type && t.addedNodes.forEach((t)=>{ + t.nodeType === Node.ELEMENT_NODE && (t.classList.contains("cbi-input-invalid") && error_tooltips_i(t), t.querySelectorAll(".cbi-input-invalid").forEach((t)=>{ + error_tooltips_i(t); + })); + }); + }); + }).observe(document.body, { + attributes: !0, + childList: !0, + subtree: !0, + attributeFilter: [ + "class", + "data-tooltip" + ] + }); +} +function error_tooltips_e(t) { + return t.parentElement?.closest(".control-group, .password-input, .cbi-input-group, .add-item") || t; +} +function error_tooltips_i(i) { + let r = i.getAttribute("data-tooltip"); + if (!r) return; + let n = error_tooltips_e(i), l = n.nextElementSibling; + if (l?.classList.contains("fluent-error-message")) { + let t = l.querySelector(".fluent-error-text"); + t && t.textContent !== r && (t.textContent = r), l.style.display = ""; + return; + } + (l = jsx("div", { + class: "fluent-error-message", + children: jsx("span", { + class: "fluent-error-text", + children: r + }) + })).insertAdjacentHTML("afterbegin", ''), n.parentNode?.insertBefore(l, n.nextSibling); +} + +;// CONCATENATED MODULE: ./web/resources/utils/direction.ts +function direction_t(t) { + return "rtl" === t || "ltr" === t ? t : null; +} +function getEffectiveDocumentDirection(e = document) { + let n = direction_t(e.documentElement.dir); + return n || (direction_t(e.body?.dataset.fluentDir) ?? "ltr"); +} +function getViewportInlineSize() { + return window.innerWidth; +} +function getRectInlineStart(t, e, n = getViewportInlineSize()) { + return "rtl" === e ? n - t.right : t.left; +} +function getPhysicalLeftFromInlineStart(t, e, n) { + return "rtl" === e ? n - t.inlineStart - t.inlineSize : t.inlineStart; +} +function applyInlineGeometryToStyle(t, e) { + t.left = "auto", t.insetInlineStart = `${e.inlineStart}px`, t.width = `${e.inlineSize}px`; +} +function setInlineCssCustomProperties(t, e, n, i, r) { + t.setProperty(r.inlineStart, `${getPhysicalLeftFromInlineStart(e, n, i)}px`), t.setProperty(r.inlineSize, `${e.inlineSize}px`); +} +function getInlinePadding(t) { + return { + inlineStart: Number.parseFloat(t.paddingInlineStart) || 16, + inlineEnd: Number.parseFloat(t.paddingInlineEnd) || 16 + }; +} + +;// CONCATENATED MODULE: ./web/resources/utils/ifacebox-tooltip.ts + +let ifacebox_tooltip_l = null; +function setupIfaceboxTooltips() { + document.addEventListener("mouseover", (n)=>{ + let s = n.target.closest(".cbi-tooltip-container"); + if (!s?.closest('td[data-name="_ifacebox"] .ifacebox-body, .td[data-name="_ifacebox"] .ifacebox-body')) { + ifacebox_tooltip_l && "none" !== ifacebox_tooltip_l.style.display && (ifacebox_tooltip_l.style.display = "none", ifacebox_tooltip_l.style.opacity = "0"); + return; + } + let a = s.querySelector(".cbi-tooltip"); + if (!a) return; + ifacebox_tooltip_l || ((ifacebox_tooltip_l = document.createElement("div")).id = "fluent-global-tooltip", ifacebox_tooltip_l.className = "cbi-tooltip ifacebadge large", ifacebox_tooltip_l.style.position = "absolute", ifacebox_tooltip_l.style.zIndex = "10000", ifacebox_tooltip_l.style.pointerEvents = "none", ifacebox_tooltip_l.style.display = "none", document.body.appendChild(ifacebox_tooltip_l)), ifacebox_tooltip_l.innerHTML = a.innerHTML, ifacebox_tooltip_l.style.display = "block", ifacebox_tooltip_l.style.opacity = "0"; + let d = s.getBoundingClientRect(), r = ifacebox_tooltip_l.getBoundingClientRect(), y = getEffectiveDocumentDirection(), c = getViewportInlineSize(), p = window.pageYOffset + d.bottom + 6, f = getRectInlineStart(d, y, c) + d.width / 2, m = f - r.width / 2; + m < 10 && (m = 10), m + r.width > c - 10 && (m = c - r.width - 10); + let u = getPhysicalLeftFromInlineStart({ + inlineStart: m, + inlineSize: r.width + }, y, c); + ifacebox_tooltip_l.style.top = `${p}px`, ifacebox_tooltip_l.style.left = `${window.pageXOffset + u}px`, ifacebox_tooltip_l.offsetWidth, ifacebox_tooltip_l.style.opacity = "1"; + let v = f - m, w = "rtl" === y ? r.width - v : v; + ifacebox_tooltip_l.style.setProperty("--arrow-left", `${w}px`); + let b = ()=>{ + ifacebox_tooltip_l && (ifacebox_tooltip_l.style.display = "none", ifacebox_tooltip_l.style.opacity = "0"), s.removeEventListener("mouseleave", b), window.removeEventListener("scroll", g, !0); + }, g = ()=>{ + ifacebox_tooltip_l && (ifacebox_tooltip_l.style.display = "none", ifacebox_tooltip_l.style.opacity = "0"), s.removeEventListener("mouseleave", b), window.removeEventListener("scroll", g, !0); + }; + s.addEventListener("mouseleave", b), window.addEventListener("scroll", g, !0); + }, !0); +} + +;// CONCATENATED MODULE: ./web/resources/utils/log-viewer.tsx + +let log_viewer_l = "fluent-log-wordwrap", log_viewer_n = "fluent-log-autoscroll"; +function log_viewer_o(e) { + try { + let t = document.createElement("textarea"); + t.value = e, t.style.position = "fixed", t.style.left = "-9999px", t.style.top = "-9999px", document.body.appendChild(t), t.focus(), t.select(); + let l = document.execCommand("copy"); + return document.body.removeChild(t), l; + } catch (e) { + return !1; + } +} +function setupLogViewer() { + let e = document.getElementById("syslog"); + e && log_viewer_r(e), new MutationObserver(()=>{ + let e = document.getElementById("syslog"); + e && "true" !== e.getAttribute("data-fluent-log-transformed") && log_viewer_r(e); + }).observe(document.body, { + childList: !0, + subtree: !0 + }); +} +function log_viewer_r(r) { + if ("true" === r.getAttribute("data-fluent-log-transformed")) return; + r.setAttribute("data-fluent-log-transformed", "true"); + let i = "true" === localStorage.getItem(log_viewer_l), a = "false" !== localStorage.getItem(log_viewer_n), s = !1, c = null, u = null, d = jsxs("div", { + class: "fluent-log-viewer", + children: [ + jsxs("div", { + class: "fluent-log-viewer__header", + children: [ + jsxs("div", { + class: "fluent-log-viewer__stats", + children: [ + jsxs("span", { + class: "fluent-log-viewer__badge fluent-log-viewer__badge--lines", + children: [ + jsx("span", { + class: "fluent-log-viewer__badge-icon fluent-log-viewer__badge-icon--lines" + }), + jsx("span", { + class: "fluent-log-viewer__badge-count", + id: "fluentLogCountLines", + children: "0" + }), + jsx("span", { + class: "fluent-log-viewer__badge-label", + children: _("Lines") + }) + ] + }), + jsxs("span", { + class: "fluent-log-viewer__badge fluent-log-viewer__badge--errors", + children: [ + jsx("span", { + class: "fluent-log-viewer__badge-icon fluent-log-viewer__badge-icon--errors" + }), + jsx("span", { + class: "fluent-log-viewer__badge-count", + id: "fluentLogCountErrors", + children: "0" + }), + jsx("span", { + class: "fluent-log-viewer__badge-label", + children: _("Errors") + }) + ] + }), + jsxs("span", { + class: "fluent-log-viewer__badge fluent-log-viewer__badge--warnings", + children: [ + jsx("span", { + class: "fluent-log-viewer__badge-icon fluent-log-viewer__badge-icon--warnings" + }), + jsx("span", { + class: "fluent-log-viewer__badge-count", + id: "fluentLogCountWarnings", + children: "0" + }), + jsx("span", { + class: "fluent-log-viewer__badge-label", + children: _("Warnings") + }) + ] + }), + jsxs("span", { + class: "fluent-log-viewer__badge fluent-log-viewer__badge--disconnects", + children: [ + jsx("span", { + class: "fluent-log-viewer__badge-icon fluent-log-viewer__badge-icon--disconnects" + }), + jsx("span", { + class: "fluent-log-viewer__badge-count", + id: "fluentLogCountDisconnects", + children: "0" + }), + jsx("span", { + class: "fluent-log-viewer__badge-label", + children: _("Disconnects") + }) + ] + }) + ] + }), + jsxs("div", { + class: "fluent-log-viewer__toolbar", + children: [ + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "wrap", + title: _("Word wrap"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--wrap" + }) + }), + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "autoscroll", + title: _("Auto-scroll"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--autoscroll" + }) + }), + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "copy", + title: _("Copy log"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--copy" + }) + }), + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "download", + title: _("Download log"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--download" + }) + }), + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "scroll-top", + title: _("Scroll to top"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--scroll-top" + }) + }), + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "scroll-bottom", + title: _("Scroll to bottom"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--scroll-bottom" + }) + }), + jsx("button", { + type: "button", + class: "fluent-log-viewer__tool-btn", + "data-action": "fullscreen", + title: _("Toggle fullscreen"), + children: jsx("span", { + class: "fluent-log-viewer__icon fluent-log-viewer__icon--fullscreen" + }) + }) + ] + }) + ] + }), + jsx("div", { + class: "fluent-log-viewer__body", + children: jsx("div", { + class: "fluent-log-viewer__content" + }) + }) + ] + }), g = d.querySelector("#fluentLogCountLines"), f = d.querySelector("#fluentLogCountErrors"), v = d.querySelector("#fluentLogCountWarnings"), p = d.querySelector("#fluentLogCountDisconnects"), w = d.querySelector(".fluent-log-viewer__body"), b = d.querySelector(".fluent-log-viewer__content"), m = d.querySelector('[data-action="wrap"]'), h = d.querySelector('[data-action="autoscroll"]'), y = d.querySelector('[data-action="copy"]'), L = d.querySelector('[data-action="download"]'), C = d.querySelector('[data-action="scroll-top"]'), x = d.querySelector('[data-action="scroll-bottom"]'), S = d.querySelector('[data-action="fullscreen"]'); + function E(e, t) { + let l = e.getAttribute("title") || ""; + e.setAttribute("title", t), e.classList.add("fluent-log-viewer__tool-btn--feedback"), setTimeout(()=>{ + e.setAttribute("title", l), e.classList.remove("fluent-log-viewer__tool-btn--feedback"); + }, 1500); + } + function q() { + if (!r) return; + let { lines: e, errors: t, warnings: l, disconnects: n } = function(e) { + let t = e.split(/\r?\n/), l = [], n = 0, o = 0, r = 0, i = 0; + for(let e = 0; e < t.length; e++){ + let a, s, c, u = t[e]; + if (!u && e === t.length - 1) continue; + i++; + let d = "default", g = u, f = u.match(/^\[([^\]]+)\]\s+([a-zA-Z0-9_-]+\.([a-zA-Z0-9_-]+))\s*:\s*(?:([a-zA-Z0-9_./-]+(?:\[\d+\])?)\s*:\s*)?(.*)$/); + if (f) { + a = f[1], s = f[2]; + let e = f[3].toLowerCase(); + c = f[4], g = f[5], [ + "err", + "error", + "crit", + "alert", + "emerg" + ].includes(e) ? (d = "err", n++) : [ + "warn", + "warning" + ].includes(e) ? (d = "warn", o++) : [ + "notice", + "info" + ].includes(e) ? d = "notice" === e ? "notice" : "info" : "debug" === e && (d = "debug"); + } else { + let e = u.match(/^\[\s*([\d.]+)\]\s+(.*)$/); + if (e) { + a = e[1]; + let t = (g = e[2]).toLowerCase(); + t.includes("cut here") || t.includes("panic") || t.includes("error") || t.includes("failed") ? (d = "err", n++) : (t.includes("warning") || t.includes("warn")) && (d = "warn", o++); + } + } + let v = /disconnect|disassociat|deauthenticat|Exited normally|Exit before auth/i.test(u); + v && r++, l.push({ + lineNumber: i, + raw: u, + time: a, + level: s, + levelType: d, + tag: c, + message: g, + isDisconnect: v + }); + } + return { + lines: l, + errors: n, + warnings: o, + disconnects: r + }; + }(r.value || r.textContent || ""); + g.textContent = String(e.length), f.textContent = String(t), v.textContent = String(l), p.textContent = String(n); + let o = document.createDocumentFragment(); + e.forEach((e)=>{ + let t = document.createElement("div"); + t.className = `fluent-log-viewer__line fluent-log-viewer__line--${e.levelType}${e.isDisconnect ? " fluent-log-viewer__line--disconnect" : ""}`; + let l = document.createElement("span"); + if (l.className = "fluent-log-viewer__line-number", l.textContent = String(e.lineNumber), l.title = _("Line %d").format(e.lineNumber), t.appendChild(l), e.time) { + let l = document.createElement("span"); + l.className = "fluent-log-viewer__line-time", l.textContent = function(e) { + if (/^\s*[\d.]+\s*$/.test(e)) { + let t = parseFloat(e); + return Number.isNaN(t) ? e.trim() : `${t.toFixed(2)}s`; + } + let t = e.match(/(\d{2}:\d{2}:\d{2})/); + if (t) { + let l = { + jan: "01", + feb: "02", + mar: "03", + apr: "04", + may: "05", + jun: "06", + jul: "07", + aug: "08", + sep: "09", + oct: "10", + nov: "11", + dec: "12" + }, n = e.match(/(\d{1,2})\s+([A-Za-z]{3})/i); + if (n) { + let e = n[1].padStart(2, "0"), o = n[2].toLowerCase(); + if (l[o]) return `${l[o]}-${e} ${t[1]}`; + } + let o = e.match(/([A-Za-z]{3})\s+(\d{1,2})/i); + if (o) { + let e = o[1].toLowerCase(), n = o[2].padStart(2, "0"); + if (l[e]) return `${l[e]}-${n} ${t[1]}`; + } + return t[1]; + } + return e; + }(e.time), t.appendChild(l); + } + if (e.level) { + let l = document.createElement("span"); + l.className = `fluent-log-viewer__level fluent-log-viewer__level--${e.levelType}`, l.textContent = e.level, t.appendChild(l); + } + if (e.tag) { + let l = document.createElement("span"); + l.className = "fluent-log-viewer__tag", l.textContent = `${e.tag}:`, t.appendChild(l); + } + let n = document.createElement("span"); + n.className = "fluent-log-viewer__text", n.textContent = e.message, t.appendChild(n), o.appendChild(t); + }), b.innerHTML = "", b.appendChild(o), a && requestAnimationFrame(()=>{ + w.scrollTop = w.scrollHeight; + }); + } + i && (m.classList.add("active"), d.classList.add("fluent-log-viewer--wrap")), a && h.classList.add("active"), r.style.display = "none", r.parentNode && r.parentNode.insertBefore(d, r.nextSibling), m.addEventListener("click", ()=>{ + i = !i, m.classList.toggle("active", i), d.classList.toggle("fluent-log-viewer--wrap", i); + try { + localStorage.setItem(log_viewer_l, i ? "true" : "false"); + } catch (e) {} + }), h.addEventListener("click", ()=>{ + a = !a, h.classList.toggle("active", a); + try { + localStorage.setItem(log_viewer_n, a ? "true" : "false"); + } catch (e) {} + a && requestAnimationFrame(()=>{ + w.scrollTop = w.scrollHeight; + }); + }), y.addEventListener("click", ()=>{ + var e; + (e = r.value || r.textContent || "", navigator.clipboard && window.isSecureContext ? navigator.clipboard.writeText(e).then(()=>!0).catch(()=>log_viewer_o(e)) : Promise.resolve(log_viewer_o(e))).then((e)=>{ + e && E(y, _("Copied")); + }); + }), L.addEventListener("click", ()=>{ + let e = r.value || r.textContent || "", t = location.pathname.includes("dmesg"), l = new Date().toISOString().slice(0, 10), n = `${t ? "dmesg" : "syslog"}-${l}.log`, o = new Blob([ + e + ], { + type: "text/plain;charset=utf-8" + }), i = URL.createObjectURL(o), a = document.createElement("a"); + a.href = i, a.download = n, document.body.appendChild(a), a.click(), document.body.removeChild(a), URL.revokeObjectURL(i), E(L, _("Downloaded")); + }), C.addEventListener("click", ()=>{ + w.scrollTo({ + top: 0, + behavior: "smooth" + }); + }), x.addEventListener("click", ()=>{ + w.scrollTo({ + top: w.scrollHeight, + behavior: "smooth" + }); + }), S.addEventListener("click", ()=>{ + s = !s, S.classList.toggle("active", s), s ? (c = d.parentNode, u = d.nextSibling, document.body.appendChild(d), d.classList.add("fluent-log-viewer--fullscreen"), document.body.classList.add("fluent-log-fullscreen")) : (c && c.insertBefore(d, u), d.classList.remove("fluent-log-viewer--fullscreen"), document.body.classList.remove("fluent-log-fullscreen")); + let e = S.querySelector(".fluent-log-viewer__icon"); + e && (e.classList.toggle("fluent-log-viewer__icon--fullscreen", !s), e.classList.toggle("fluent-log-viewer__icon--fullscreen-exit", s)); + }), q(), new MutationObserver(()=>{ + q(); + }).observe(r, { + childList: !0, + characterData: !0, + subtree: !0, + attributes: !0 + }); + let A = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set; + A && Object.defineProperty(r, "value", { + set (e) { + A.call(this, e), q(); + }, + get () { + return Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.get?.call(this); + }, + configurable: !0 + }), r.addEventListener("input", q), r.addEventListener("change", q); +} + +;// CONCATENATED MODULE: ./web/resources/utils/mac-selector.tsx + +let mac_selector_n = [ + { + name: _("Random Local MAC"), + prefix: "RANDOM_LOCAL" + }, + { + name: "Apple", + prefix: "00:1C:42" + }, + { + name: "Apple", + prefix: "AC:BC:32" + }, + { + name: "Intel", + prefix: "00:1E:67" + }, + { + name: "Intel", + prefix: "3C:FD:FE" + }, + { + name: "Huawei", + prefix: "00:1E:10" + }, + { + name: "Huawei", + prefix: "70:54:F5" + }, + { + name: "Xiaomi", + prefix: "18:59:36" + }, + { + name: "Xiaomi", + prefix: "34:80:B3" + }, + { + name: "ASUS", + prefix: "04:D4:C4" + }, + { + name: "ASUS", + prefix: "2C:4D:54" + }, + { + name: "TP-Link", + prefix: "00:1D:0F" + }, + { + name: "TP-Link", + prefix: "50:C7:BF" + }, + { + name: "Lenovo", + prefix: "00:12:FE" + }, + { + name: "Dell", + prefix: "00:14:22" + }, + { + name: "Microsoft", + prefix: "00:15:5D" + }, + { + name: "Raspberry Pi", + prefix: "B8:27:EB" + }, + { + name: "Raspberry Pi", + prefix: "DC:A6:32" + }, + { + name: "Realtek / QEMU", + prefix: "52:54:00" + }, + { + name: "Cisco", + prefix: "00:00:0C" + }, + { + name: "Samsung", + prefix: "00:12:FB" + } +]; +function mac_selector_a() { + return Math.floor(256 * Math.random()).toString(16).padStart(2, "0").toUpperCase(); +} +function mac_selector_l(l) { + let i; + if ("true" === l.dataset.fluentMacInit) return; + l.dataset.fluentMacInit = "true"; + let r = l.parentElement; + if (!r) return; + r.classList.contains("fluent-mac-selector-group") ? i = r : (i = jsx("div", { + class: "cbi-input-group fluent-mac-selector-group" + }), r.insertBefore(i, l), i.appendChild(l)); + let c = jsx("button", { + type: "button", + class: "cbi-button cbi-button-action fluent-mac-edit-btn", + title: _("Advanced MAC Selector") + }); + c.addEventListener("click", (i)=>{ + let r, c, o, d, s, p, u, f, m, b, v; + i.preventDefault(), i.stopPropagation(), c = function(e) { + if (!e) return null; + let t = e.trim().toUpperCase().replace(/[:-]/g, ""); + if (t.length < 6) return null; + let a = t.slice(0, 6), l = `${a.slice(0, 2)}:${a.slice(2, 4)}:${a.slice(4, 6)}`, i = mac_selector_n.find((e)=>e.prefix.toUpperCase() === l); + return i ? i.prefix : null; + }(r = l.value.trim() || l.placeholder || "00:00:00:00:00:00"), d = jsx("input", { + id: o = "fluent-mac-overlay-input", + type: "text", + class: "cbi-input-text fluent-mac-modal-input", + value: r, + placeholder: "00:00:00:00:00:00" + }), s = jsx("select", { + id: "fluent-mac-overlay-vendor-select", + class: "cbi-input-select", + style: "width: 100%;", + children: jsx("option", { + value: "", + disabled: !0, + hidden: !0, + selected: !c, + children: _("Select vendor prefix...") + }) + }), mac_selector_n.forEach((e)=>{ + let t = document.createElement("option"); + t.value = e.prefix, t.textContent = `${e.name} (${"RANDOM_LOCAL" === e.prefix ? "x2:xx:xx" : e.prefix})`, c && e.prefix === c && (t.selected = !0), s.appendChild(t); + }), c && (s.value = c), (p = jsx("button", { + type: "button", + class: "btn cbi-button cbi-button-action fluent-mac-random-btn", + children: _("Randomize MAC") + })).addEventListener("click", (e)=>{ + e.stopPropagation(), d.value = function(e) { + if (e && "RANDOM_LOCAL" !== e) { + let t = e.trim().toUpperCase().split(/[:-]/); + for(; t.length < 6;)t.push(mac_selector_a()); + return t.slice(0, 6).join(":"); + } + let t = [ + "2", + "6", + "A", + "E" + ], n = Math.floor(16 * Math.random()).toString(16).toUpperCase(), l = t[Math.floor(Math.random() * t.length)], i = [ + `${n}${l}` + ]; + for(let e = 0; e < 5; e++)i.push(mac_selector_a()); + return i.join(":"); + }(s.value || void 0); + }), u = jsx("button", { + type: "button", + class: "btn cbi-button cbi-button-neutral", + children: _("Cancel") + }), f = jsx("button", { + type: "button", + class: "btn cbi-button cbi-button-positive", + children: _("Confirm") + }), m = jsx("button", { + type: "button", + class: "fluent-mac-overlay-close", + title: _("Close") + }), b = jsx("div", { + class: "fluent-mac-overlay-backdrop", + children: jsxs("div", { + class: "fluent-mac-overlay-card", + children: [ + jsxs("div", { + class: "fluent-mac-overlay-header", + children: [ + jsx("h3", { + children: _("Advanced MAC Selector") + }), + m + ] + }), + jsxs("div", { + class: "fluent-mac-overlay-body", + children: [ + jsxs("div", { + class: "fluent-mac-field-block", + children: [ + jsx("label", { + htmlFor: o, + class: "fluent-mac-label", + children: _("Current / Pending MAC Address") + }), + d + ] + }), + jsxs("div", { + class: "fluent-mac-field-block", + children: [ + jsx("label", { + htmlFor: "fluent-mac-overlay-vendor-select", + class: "fluent-mac-label", + children: _("Preset Vendor / Generator") + }), + jsxs("div", { + class: "fluent-mac-picker-row", + children: [ + jsx("div", { + class: "fluent-mac-vendor-picker-wrap", + children: s + }), + p + ] + }) + ] + }) + ] + }), + jsxs("div", { + class: "fluent-mac-overlay-footer", + children: [ + u, + f + ] + }) + ] + }) + }), v = ()=>{ + b.remove(); + }, m.addEventListener("click", (e)=>{ + e.stopPropagation(), v(); + }), u.addEventListener("click", (e)=>{ + e.stopPropagation(), v(); + }), f.addEventListener("click", (e)=>{ + e.stopPropagation(); + let t = d.value.trim(); + t && (l.value = t, l.dispatchEvent(new Event("input", { + bubbles: !0 + })), l.dispatchEvent(new Event("change", { + bubbles: !0 + }))), v(); + }), b.addEventListener("click", (e)=>{ + e.target === b && v(); + }), document.body.appendChild(b); + }), i.appendChild(c); +} +function setupMacSelector() { + document.querySelectorAll('input[id$=".macaddr"]').forEach((e)=>{ + mac_selector_l(e); + }), new MutationObserver((e)=>{ + e.forEach((e)=>{ + e.addedNodes.forEach((e)=>{ + e.nodeType === Node.ELEMENT_NODE && ("INPUT" === e.tagName && e.id && e.id.endsWith(".macaddr") ? mac_selector_l(e) : e.querySelectorAll('input[id$=".macaddr"]').forEach((e)=>{ + mac_selector_l(e); + })); + }); + }); + }).observe(document.body, { + childList: !0, + subtree: !0 + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/menu-search.ts +let menu_search_e; +function menu_search_t(e) { + return e.toLowerCase().replace(/\s+/g, " "); +} +function menu_search_n(e, n, l) { + return menu_search_t(n).includes(l) || menu_search_t(e).includes(l); +} +function searchMenu(e, l) { + let r = menu_search_t(l); + if (!r) return []; + let a = [], i = (e, t, l)=>{ + for (let u of ui.menu.getChildren(e)){ + if (!u.satisfied) continue; + let e = `${t}/${u.name}`, c = u.title || u.name || "", s = c ? _(c) : "", o = c ? [ + ...l, + s + ] : l; + menu_search_n(c, s, r) && a.push({ + node: u, + url: e, + breadcrumb: [ + ...o + ] + }), i(u, e, o); + } + }; + for (let t of e.categories)if (!e.hiddenCategoryIds.has(t.id)) for (let l of (t.primary && menu_search_n(t.primary.rawTitle, t.title, r) && a.push({ + node: t.primary.node, + url: t.primary.path, + breadcrumb: [ + t.title + ] + }), t.items)){ + if (e.hiddenPaths.has(l.path)) continue; + let u = [ + t.title, + l.title + ]; + menu_search_n(l.rawTitle, l.title, r) && a.push({ + node: l.node, + url: l.path, + breadcrumb: u + }), i(l.node, l.path, u); + } + return a; +} +function setupMenuSearch(t) { + menu_search_e && (document.removeEventListener("keydown", menu_search_e), menu_search_e = void 0); + let n = [], l = (e)=>{ + let l = document.querySelector(e); + if (!l) return; + l.innerHTML = ""; + let { container: r, input: a } = function(e) { + let t = document.createElement("div"); + t.className = "fluent-menu-search"; + let n = document.createElement("input"); + n.type = "search", n.className = "fluent-menu-search-input", n.placeholder = _("Search menu..."), n.autocomplete = "off", n.spellcheck = !1, n.setAttribute("aria-label", _("Search menu items")); + let l = document.createElement("span"); + l.className = "fluent-menu-search-hotkey", l.textContent = "Ctrl+K"; + let r = document.createElement("div"); + r.className = "fluent-menu-search-overlay", r.style.display = "none", t.appendChild(n), t.appendChild(l), t.appendChild(r); + let a = -1, i = []; + function u() { + r.style.display = "none", r.innerHTML = "", a = -1, i = []; + } + function c(e) { + u(), n.blur(), window.location.href = L.url(e.url); + } + function s(e) { + let t = r.querySelectorAll(".fluent-menu-search-item"); + 0 !== t.length && (a >= 0 && t[a] && (t[a].classList.remove("selected"), t[a].removeAttribute("aria-selected")), (a += e) < 0 && (a = t.length - 1), a >= t.length && (a = 0), t[a].classList.add("selected"), t[a].setAttribute("aria-selected", "true"), t[a].scrollIntoView({ + block: "nearest" + })); + } + return n.addEventListener("input", ()=>{ + let t = n.value.trim(); + t ? function(e) { + if (i = e, r.innerHTML = "", a = -1, 0 === e.length) { + r.style.display = "none"; + return; + } + let t = document.createElement("ul"); + t.className = "fluent-menu-search-list", t.setAttribute("role", "listbox"); + let l = Math.min(e.length, 20); + for(let n = 0; n < l; n++){ + let l = e[n], r = document.createElement("li"); + r.className = "fluent-menu-search-item", r.setAttribute("role", "option"), r.setAttribute("data-index", String(n)); + let a = document.createElement("span"); + if (a.className = "fluent-menu-search-label", a.textContent = _(l.node.title || l.node.name || ""), l.breadcrumb.length > 1) { + let e = document.createElement("span"); + e.className = "fluent-menu-search-path", e.textContent = l.breadcrumb.slice(0, -1).join(" \u2192 "), r.appendChild(e); + } + r.appendChild(a), r.addEventListener("mousedown", (e)=>{ + e.preventDefault(), c(l); + }), r.addEventListener("click", ()=>c(l)), t.appendChild(r); + } + if (e.length > 20) { + let n = document.createElement("li"); + n.className = "fluent-menu-search-more", n.textContent = _("and %d more...").format(e.length - 20), t.appendChild(n); + } + r.appendChild(t), r.style.display = ""; + let u = n.getBoundingClientRect(); + r.style.left = "0", r.style.top = `${u.height}px`; + }(searchMenu(e, t)) : u(); + }), n.addEventListener("keydown", (e)=>{ + if ("Escape" === e.key) { + u(), n.blur(); + return; + } + if ("ArrowDown" === e.key) { + e.preventDefault(), s(1); + return; + } + if ("ArrowUp" === e.key) { + e.preventDefault(), s(-1); + return; + } + if ("Enter" === e.key) { + e.preventDefault(); + let t = r.querySelectorAll(".fluent-menu-search-item"); + a >= 0 && t[a] ? t[a].click() : i.length > 0 && c(i[0]); + return; + } + }), n.addEventListener("blur", ()=>{ + setTimeout(()=>{ + r.contains(document.activeElement) || u(); + }, 150); + }), r.addEventListener("mousedown", (e)=>e.preventDefault()), { + container: t, + input: n, + overlay: r + }; + }(t); + l.appendChild(r), n.push(a); + }; + l(".header__search-slot"), l(".sidebar__search-slot"), 0 !== n.length && (menu_search_e = (e)=>{ + if ((e.ctrlKey || e.metaKey) && "k" === e.key || "/" === e.key && !e.ctrlKey && !e.metaKey && !e.altKey && document.activeElement?.tagName !== "INPUT" && document.activeElement?.tagName !== "TEXTAREA" && !document.activeElement?.getAttribute("contenteditable")) { + e.preventDefault(), (()=>{ + for (let e of n)if (null !== e.offsetParent) { + e.focus(), e.select(); + return; + } + })(); + return; + } + let t = n.find((e)=>document.activeElement === e); + "Escape" === e.key && t && t.blur(); + }, document.addEventListener("keydown", menu_search_e)); +} + +;// CONCATENATED MODULE: ./web/resources/utils/poll-pause.ts +function setupSelectionPause() { + let t = !1, e = L.poll, n = "u" < typeof XHR ? void 0 : XHR, o = "function" == typeof e?.active && "function" == typeof e.stop && "function" == typeof e.start ? e : null, i = "function" == typeof n?.running && "function" == typeof n.halt && "function" == typeof n.run ? n : null, c = o ? { + isActive: ()=>o.active(), + stop: ()=>o.stop(), + start: ()=>o.start() + } : i ? { + isActive: ()=>i.running(), + stop: ()=>i.halt(), + start: ()=>i.run() + } : null; + c && document.addEventListener("selectionchange", ()=>{ + let e = document.getSelection(), n = (e?.toString().trim().length ?? 0) > 0; + if (!n) { + let t = document.activeElement; + (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement) && (n = t.selectionStart !== t.selectionEnd); + } + n ? !t && c.isActive() && (t = c.stop()) : t && (c.start(), t = !1); + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/select-dropdown.tsx + + +let select_dropdown_r = !1; +function select_dropdown_s() { + document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{ + if (e instanceof HTMLElement) if (e.classList.contains("fluent-custom-select")) { + let t = e.previousElementSibling; + t instanceof HTMLSelectElement && select_dropdown_f(e, t); + } else select_dropdown_a(e); + }); +} +function select_dropdown_a(e) { + let t = e.getBoundingClientRect(), n = window.innerHeight - t.bottom, o = t.top, l = e.querySelector("ul.dropdown, ul:not(.preview)"), i = l ? l.querySelectorAll("li").length : 8, r = l ? l.scrollHeight : 0, s = Math.min(r > 0 ? r + 8 : 36 * i + 10, select_dropdown_c), a = n < s && o > n, d = a ? Math.max(64, o - select_dropdown_u - select_dropdown_p) : Math.max(64, n - select_dropdown_u - select_dropdown_p); + e.setAttribute("data-open-direction", a ? "up" : "down"), e.style.setProperty("--fluent-dropdown-max-height", `${Math.min(select_dropdown_c, s, d)}px`); +} +function setupFluentSelects() { + select_dropdown_r || (select_dropdown_r = !0, window.addEventListener("scroll", select_dropdown_s, !0), window.addEventListener("resize", select_dropdown_s), new MutationObserver((e)=>{ + e.forEach((e)=>{ + if ("attributes" === e.type && "open" === e.attributeName) { + let t = e.target; + if (t.hasAttribute("open")) if (t.classList.contains("fluent-custom-select")) { + let e = t.previousElementSibling; + e instanceof HTMLSelectElement && select_dropdown_f(t, e); + } else ("CBI-DROPDOWN" === t.tagName || t.classList.contains("cbi-dropdown")) && select_dropdown_a(t); + } + }); + }).observe(document.body, { + attributes: !0, + attributeFilter: [ + "open" + ], + subtree: !0 + })), "0" === document.body.getAttribute("data-theme-custom-select") || (document.querySelectorAll("select").forEach((e)=>{ + select_dropdown_h(e); + }), document.querySelectorAll("cbi-dropdown, .cbi-dropdown").forEach((e)=>{ + e.classList.contains("fluent-custom-select") || select_dropdown_d(e); + }), new MutationObserver((e)=>{ + e.forEach((e)=>{ + e.addedNodes.forEach((e)=>{ + e.nodeType === Node.ELEMENT_NODE && ("SELECT" === e.tagName ? select_dropdown_h(e) : "CBI-DROPDOWN" === e.tagName || e.classList.contains("cbi-dropdown") ? e.classList.contains("fluent-custom-select") || select_dropdown_d(e) : (e.querySelectorAll("select").forEach((e)=>{ + select_dropdown_h(e); + }), e.querySelectorAll("cbi-dropdown, .cbi-dropdown").forEach((e)=>{ + e.classList.contains("fluent-custom-select") || select_dropdown_d(e); + }))); + }); + }); + }).observe(document.body, { + childList: !0, + subtree: !0 + })); +} +function select_dropdown_d(e) { + let t; + (t = e.querySelector("span.open")) && "true" !== t.getAttribute("data-chevron-upgraded") && (t.setAttribute("data-chevron-upgraded", "true"), t.innerHTML = ""); + let n = e.querySelector("ul:not(.preview)"); + n && !n.classList.contains("dropdown") && n.classList.add("dropdown"); +} +let select_dropdown_c = 350, select_dropdown_u = 8, select_dropdown_p = 2; +function select_dropdown_b(e, t) { + let n = t.offsetTop, o = n + t.offsetHeight, l = e.scrollTop, i = l + e.clientHeight; + n < l ? e.scrollTop = n : o > i && (e.scrollTop = o - e.clientHeight); +} +function select_dropdown_f(e, t) { + let r = e.getBoundingClientRect(), s = getEffectiveDocumentDirection(), a = getViewportInlineSize(), d = window.innerHeight, b = d - r.bottom, f = r.top; + if (b < Math.min(32 * t.options.length + 10, select_dropdown_c) && f > b ? e.setAttribute("data-open-direction", "up") : e.setAttribute("data-open-direction", "down"), !e.closest("#modal_overlay .modal, .fluent-mac-overlay-card, [class*='overlay']")) return void e.removeAttribute("data-fluent-floating"); + let m = Math.min(32 * t.options.length + 10, select_dropdown_c, 0.45 * d), h = b < m && f > b, v = Math.min(m, Math.max(64, h ? f - select_dropdown_u - select_dropdown_p : b - select_dropdown_u - select_dropdown_p)), w = Math.min(r.width, a - 2 * select_dropdown_u), y = Math.min(Math.max(select_dropdown_u, getRectInlineStart(r, s, a)), a - w - select_dropdown_u), A = h ? Math.max(select_dropdown_u, r.top - v - select_dropdown_p) : Math.min(d - select_dropdown_u - v, r.bottom + select_dropdown_p); + e.setAttribute("data-open-direction", h ? "up" : "down"), e.setAttribute("data-fluent-floating", "modal"), setInlineCssCustomProperties(e.style, { + inlineStart: y, + inlineSize: w + }, s, a, { + inlineStart: "--fluent-dropdown-left", + inlineSize: "--fluent-dropdown-width" + }), e.style.setProperty("--fluent-dropdown-top", `${A}px`), e.style.setProperty("--fluent-dropdown-max-height", `${v}px`); +} +function select_dropdown_m(e) { + e.removeAttribute("open"), e.removeAttribute("data-fluent-floating"), e.style.removeProperty("--fluent-dropdown-left"), e.style.removeProperty("--fluent-dropdown-top"), e.style.removeProperty("--fluent-dropdown-width"), e.style.removeProperty("--fluent-dropdown-max-height"), e.closest(".cbi-value-field, .cbi-value")?.classList.remove("cbi-dropdown-open"); + let t = e.querySelector("ul.dropdown"); + t instanceof HTMLElement && "function" == typeof t.hidePopover && t.matches(":popover-open") && t.hidePopover(); +} +function select_dropdown_h(n) { + if (n?.tagName !== "SELECT" || "true" === n.getAttribute("data-fluent-transformed") || n.closest(".cbi-dropdown") || n.multiple || function(e) { + let t = e.getAttribute("style") || ""; + if (/\bdisplay\s*:\s*none/i.test(t) || "none" === e.style.display) return !0; + if (!e.isConnected) return !1; + try { + let t = window.getComputedStyle(e); + if ("none" === t.display) { + let t = e.parentElement, n = !1; + for(; t;){ + let e = window.getComputedStyle(t); + if ("none" === e.display) { + n = !0; + break; + } + t = t.parentElement; + } + if (!n) return !0; + let o = e.cloneNode(!1); + o.style.display = "", document.body.appendChild(o); + let l = window.getComputedStyle(o), i = "none" === l.display; + return document.body.removeChild(o), i; + } + } catch (e) {} + return !1; + }(n)) return; + let o = n.getAttribute("style"); + n.setAttribute("data-fluent-transformed", "true"), n.style.setProperty("display", "none", "important"); + let l = jsx("li", {}); + l.setAttribute("selected", ""); + let i = jsx("ul", { + class: "dropdown" + }), r = jsx("span", { + class: "open" + }), s = jsxs("div", { + class: "fluent-custom-select", + children: [ + jsx("ul", { + children: l + }), + r, + i + ] + }); + if (s.setAttribute("tabindex", "0"), o) { + let e = o.replace(/\bdisplay\s*:\s*[^;]+(;|$)/gi, "").trim(); + e && s.setAttribute("style", e); + } + n.disabled && (s.setAttribute("disabled", ""), s.removeAttribute("tabindex")); + let a = ()=>{ + i.innerHTML = ""; + let t = "", o = !1; + if (Array.from(n.options).forEach((n)=>{ + let l = jsx("li", { + children: n.text + }); + l.setAttribute("data-value", n.value), n.selected && (l.setAttribute("selected", ""), t = n.text, o = !0), n.disabled && l.setAttribute("disabled", ""), i.appendChild(l); + }), !o && n.options.length > 0) { + let e = n.options[n.selectedIndex >= 0 ? n.selectedIndex : 0]; + t = e.text; + let o = i.querySelector(`li[data-value="${e.value}"]`); + o?.setAttribute("selected", ""); + } + l.textContent = t; + }; + a(), n.parentNode?.insertBefore(s, n.nextSibling), s.addEventListener("click", (e)=>{ + if (s.hasAttribute("disabled")) return; + let t = e.target.closest("ul.dropdown > li"); + if (t) { + let o = t.getAttribute("data-value"); + null === o || t.hasAttribute("disabled") || (n.value = o, n.dispatchEvent(new Event("change", { + bubbles: !0 + })), n.dispatchEvent(new Event("input", { + bubbles: !0 + }))), select_dropdown_m(s), e.stopPropagation(); + return; + } + if (s.hasAttribute("open")) select_dropdown_m(s); + else { + document.querySelectorAll("cbi-dropdown[open], .cbi-dropdown[open], .fluent-custom-select[open]").forEach((e)=>{ + e instanceof HTMLElement && e.classList.contains("fluent-custom-select") ? select_dropdown_m(e) : (e.removeAttribute("open"), e.closest(".cbi-value-field, .cbi-value")?.classList.remove("cbi-dropdown-open")); + }), select_dropdown_f(s, n), s.setAttribute("open", ""), s.closest(".cbi-value-field, .cbi-value")?.classList.add("cbi-dropdown-open"); + let e = i.querySelector("li[selected]"); + e && select_dropdown_b(i, e); + } + e.stopPropagation(); + }); + let d = (e)=>{ + !s.contains(e.target) && e.target !== n && s.hasAttribute("open") && select_dropdown_m(s); + }; + document.addEventListener("click", d, !0), s.addEventListener("keydown", (e)=>{ + if (s.hasAttribute("disabled")) return; + let t = s.hasAttribute("open"), o = Array.from(i.querySelectorAll("li:not([disabled])")), l = o.findIndex((e)=>e.hasAttribute("selected")); + switch(e.key){ + case "Enter": + case " ": + if (e.preventDefault(), t) { + let e = o[l]; + e && e.click(); + } else s.click(); + break; + case "Escape": + t && (e.preventDefault(), select_dropdown_m(s)); + break; + case "ArrowDown": + if (e.preventDefault(), t) { + if (o.length > 0) { + let e = (l + 1) % o.length; + o.forEach((t, o)=>{ + if (o === e) { + t.setAttribute("selected", ""), select_dropdown_b(i, t); + let e = t.getAttribute("data-value"); + null !== e && (n.value = e, n.dispatchEvent(new Event("change", { + bubbles: !0 + })), n.dispatchEvent(new Event("input", { + bubbles: !0 + }))); + } else t.removeAttribute("selected"); + }); + } + } else s.click(); + break; + case "ArrowUp": + if (e.preventDefault(), t) { + if (o.length > 0) { + let e = (l - 1 + o.length) % o.length; + o.forEach((t, o)=>{ + if (o === e) { + t.setAttribute("selected", ""), select_dropdown_b(i, t); + let e = t.getAttribute("data-value"); + null !== e && (n.value = e, n.dispatchEvent(new Event("change", { + bubbles: !0 + })), n.dispatchEvent(new Event("input", { + bubbles: !0 + }))); + } else t.removeAttribute("selected"); + }); + } + } else s.click(); + break; + case "Tab": + t && select_dropdown_m(s); + } + }), n.addEventListener("change", ()=>{ + let e = n.value, t = Array.from(i.querySelectorAll("li")), o = ""; + t.forEach((t)=>{ + t.getAttribute("data-value") === e ? (t.setAttribute("selected", ""), o = t.textContent || "") : t.removeAttribute("selected"); + }), l.textContent = o; + }); + let c = new MutationObserver(()=>{ + a(); + }); + c.observe(n, { + childList: !0 + }); + let u = new MutationObserver((e)=>{ + e.forEach((e)=>{ + "disabled" === e.attributeName && (n.disabled ? (s.setAttribute("disabled", ""), s.removeAttribute("tabindex"), select_dropdown_m(s)) : (s.removeAttribute("disabled"), s.setAttribute("tabindex", "0"))); + }); + }); + u.observe(n, { + attributes: !0, + attributeFilter: [ + "disabled" + ] + }); + let p = new MutationObserver((e)=>{ + e.forEach((e)=>{ + e.removedNodes.forEach((e)=>{ + (e === n || e.contains?.(n)) && (document.removeEventListener("click", d, !0), c.disconnect(), u.disconnect(), p.disconnect(), s.remove()); + }); + }); + }); + n.parentNode && p.observe(n.parentNode, { + childList: !0 + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/table-wrapper.ts +let table_wrapper_e = "table.table, table.cbi-section-table, div.table, div.cbi-section-table"; +function table_wrapper_t(e) { + let t = e.parentElement; + if (!t || t.classList.contains("table-wrapper")) return; + let n = document.createElement("div"); + n.className = "table-wrapper", t.insertBefore(n, e), n.append(e); +} +function table_wrapper_n(n) { + n instanceof HTMLElement && n.matches(table_wrapper_e) && table_wrapper_t(n), n.querySelectorAll(table_wrapper_e).forEach((e)=>{ + table_wrapper_t(e); + }); +} +function setupTableWrappers() { + document.body && (table_wrapper_n(document), new MutationObserver((e)=>{ + e.forEach((e)=>{ + e.addedNodes.forEach((e)=>{ + e instanceof Element && table_wrapper_n(e); + }); + }); + }).observe(document.body, { + childList: !0, + subtree: !0 + })); +} + +;// CONCATENATED MODULE: ./web/resources/utils/theme-features.ts + +function setupThemeFeatures() { + let l = document.body; + if (!l) return; + let a = L.ui, s = a.Dropdown; + s && (s.prototype.getScrollParent = (e)=>{ + let t = e.parentElement; + for(; t;){ + if (t.classList.contains("cbi-tabcontainer") || t.id?.startsWith("container.") || t.classList.contains("cbi-section") || t.clientHeight < 250 && !t.classList.contains("modal")) { + t = t.parentElement; + continue; + } + let e = getComputedStyle(t); + if (/(auto|scroll)/.test(e.overflow + e.overflowY + e.overflowX)) return t; + t = t.parentElement; + } + return document.scrollingElement || document.documentElement; + }); + let o = L.rpc.declare({ + object: "luci.fluent", + method: "set_mode", + params: [ + "mode" + ] + }), d = l.getAttribute("data-prefers-reduced-motion") || "1"; + if ("1" === d) { + let e = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + l.setAttribute("data-reduce-motion", e ? "true" : "false"); + } else l.setAttribute("data-reduce-motion", "false"); + "1" === d && window.matchMedia("(prefers-reduced-motion: reduce)").addEventListener("change", (e)=>{ + l.setAttribute("data-reduce-motion", e.matches ? "true" : "false"); + }); + let c = l.getAttribute("data-theme-mode") || "auto", u = document.getElementById("theme-toggle"); + function m(e) { + return "dark" === e ? "dark" : "light" === e ? "light" : window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + } + function f(e, t) { + u && (document.documentElement.setAttribute("data-theme", e), u.setAttribute("data-active-theme", e), u.setAttribute("data-mode", t)); + } + u && (f(m(l.getAttribute("data-theme-mode") || "auto"), c), u.hidden = !1, requestAnimationFrame(()=>{ + u.classList.add("visible"); + }), u.addEventListener("click", async ()=>{ + var e; + if (u.disabled) return; + let t = "dark" === (e = l.getAttribute("data-theme-mode") || c) ? "light" : "light" === e ? "auto" : "dark", i = m(t); + u.disabled = !0, f(i, t); + try { + let e = await o(t); + if (e?.result !== 0) throw Error(`RPC returned ${e?.result ?? "no response"} - permission denied or script error`); + l.setAttribute("data-theme-mode", t); + } catch (t) { + let e = l.getAttribute("data-theme-mode") || c; + f(m(e), e), a.addNotification(null, `Failed to save theme mode: ${t instanceof Error ? t.message : String(t)}`, "error"); + } finally{ + u.disabled = !1; + } + }), window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e)=>{ + if ("auto" !== (l.getAttribute("data-theme-mode") || c)) return; + let t = e.matches ? "dark" : "light"; + document.documentElement.setAttribute("data-theme", t), u.setAttribute("data-active-theme", t); + })); + let h = "1" === l.getAttribute("data-tab-animation"); + function b(l) { + let a = l.getBoundingClientRect(); + if (0 === a.width && 0 === a.height) return; + let s = l.querySelector(".fluent-tab-slider"); + s || ((s = document.createElement("div")).className = "fluent-tab-slider", l.appendChild(s)); + let o = l.querySelector("li.cbi-tab, li.active"); + if (!o) { + s.style.width = "0px"; + return; + } + let d = o.querySelector("a"); + if (!d) return; + let c = d.getBoundingClientRect(), u = window.getComputedStyle(d), m = getEffectiveDocumentDirection(), f = getViewportInlineSize(), h = getInlinePadding(u), b = { + inlineStart: getRectInlineStart(c, m, f) - getRectInlineStart(a, m, f) + Math.abs(l.scrollLeft) + h.inlineStart, + inlineSize: c.width - h.inlineStart - h.inlineEnd + }, p = `${b.inlineStart}px`, g = `${b.inlineSize}px`; + if (s.dataset.inlineStart === p && s.dataset.inlineSize === g) return; + let y = void 0 === s.dataset.inlineStart; + y && (s.style.transition = "none"), applyInlineGeometryToStyle(s.style, b), s.dataset.inlineStart = p, s.dataset.inlineSize = g, y && (s.offsetHeight, s.style.transition = ""); + } + function p() { + document.querySelectorAll("ul.cbi-tabmenu, ul.tabs").forEach((i)=>{ + if (i.dataset.sliderInit) return void b(i); + i.dataset.sliderInit = "true"; + let n = i.querySelector(".fluent-tab-slider"); + if (n || ((n = document.createElement("div")).className = "fluent-tab-slider", i.appendChild(n)), h && i.classList.contains("tabs")) { + let r = null; + try { + r = sessionStorage.getItem("fluent-tab-slider-pos"); + } catch {} + if (r) try { + let t = JSON.parse(r); + sessionStorage.removeItem("fluent-tab-slider-pos"), n.style.transition = "none", applyInlineGeometryToStyle(n.style, { + inlineStart: Number.parseFloat(t.inlineStart), + inlineSize: Number.parseFloat(t.inlineSize) + }), n.dataset.inlineStart = t.inlineStart, n.dataset.inlineSize = t.inlineSize, n.offsetHeight, n.style.transition = "", b(i); + } catch { + b(i); + } + else { + b(i); + let e = getEffectiveDocumentDirection(); + n.style.transition = "none", n.style.transformOrigin = "rtl" === e ? "right center" : "left center", n.style.transform = "scaleX(0)", n.offsetHeight, n.style.transition = "", n.style.transform = "scaleX(1)"; + } + i.querySelectorAll("li > a").forEach((e)=>{ + let t = e.getAttribute("href"); + t && "#" !== t && e.addEventListener("click", ()=>{ + try { + n && sessionStorage.setItem("fluent-tab-slider-pos", JSON.stringify({ + inlineStart: n.dataset.inlineStart || "0px", + inlineSize: n.dataset.inlineSize || "0px" + })); + } catch {} + }); + }); + } else b(i); + new MutationObserver(()=>{ + b(i); + }).observe(i, { + attributes: !0, + subtree: !0, + attributeFilter: [ + "class" + ] + }); + try { + new IntersectionObserver((e)=>{ + for (let t of e)t.isIntersecting && b(i); + }, { + threshold: 0 + }).observe(i); + } catch (e) { + console.warn("Fluent theme: IntersectionObserver not supported", e); + } + }); + } + function g() { + document.querySelectorAll("#modal_overlay .modal").forEach((e)=>{ + let t = Array.from(e.children).find((e)=>e instanceof HTMLElement && e.classList.contains("modal-content-wrap")), i = e.classList.contains("alert-message") && !e.querySelector(".button-row, button, .cbi-button"), n = e.classList.contains("spinning") && !e.querySelector(".cbi-map, form"); + if (i || n) { + if (t) { + for (let i of Array.from(t.children))e.insertBefore(i, t); + t.remove(); + } + return; + } + if (t?.querySelector(":scope > .cbi-map")) { + for (let i of Array.from(t.children))e.insertBefore(i, t); + t.remove(); + return; + } + if (Array.from(e.children).some((e)=>e.classList.contains("cbi-map"))) return; + let r = Array.from(e.children).filter((e)=>{ + if (e === t) return !1; + let i = e.classList.contains("close") || e.classList.contains("modal-close"), n = "SCRIPT" === e.tagName || "STYLE" === e.tagName; + return !i && !n; + }); + if (0 === r.length) return; + let l = [], a = 0; + for(; a < r.length;){ + let e = r[a]; + if ("H4" === e.tagName || e.classList.contains("modal-header") || e.classList.contains("cbi-tabmenu") || e.classList.contains("tabs")) l.push(e), a++; + else break; + } + let s = null; + if (a < r.length) { + let e = r[r.length - 1]; + if (!l.includes(e)) { + let t = e.classList.contains("button-row") || e.classList.contains("modal-footer") || e.classList.contains("right"), i = null !== e.querySelector('button, .btn, .cbi-button, input[type="button"], input[type="submit"]'); + if ((t || i) && (s = e, r.length >= 3)) { + let e = r[r.length - 2]; + if (!l.includes(e) && !(e.classList.contains("cbi-map") || e.classList.contains("cbi-section") || "UL" === e.tagName || "TABLE" === e.tagName || e.id.startsWith("cbi-"))) { + let t = null !== e.querySelector('input[type="checkbox"], .cbi-checkbox'), i = null !== e.querySelector('input[type="text"], input[type="number"], input[type="password"], textarea, select'); + t && !i && (s = e); + } + } + } + } + let o = [], d = s ? r.indexOf(s) : r.length; + for(let e = a; e < d; e++)o.push(r[e]); + if (o.length > 0) for (let i of (t || ((t = document.createElement("div")).className = "modal-content-wrap", s ? e.insertBefore(t, s) : e.appendChild(t)), o))t.appendChild(i); + }); + } + if (p(), g(), new MutationObserver(()=>{ + p(), g(); + }).observe(l, { + childList: !0, + subtree: !0 + }), window.addEventListener("resize", ()=>{ + document.querySelectorAll("ul.cbi-tabmenu, ul.tabs").forEach((e)=>{ + b(e); + }); + }), "1" === l.getAttribute("data-loading-bar")) { + let e = !1, t = document.getElementById("fluent-top-loading"), i = ()=>{ + t && !e && t.classList.add("loaded"); + }, n = ()=>{ + t && t.classList.remove("loaded"); + }; + "interactive" === document.readyState || "complete" === document.readyState ? i() : document.addEventListener("DOMContentLoaded", i), window.addEventListener("load", i), window.addEventListener("beforeunload", ()=>{ + e = !0, n(); + }), document.addEventListener("click", (e)=>{ + let t = e.target; + if (!t) return; + let i = t.closest("a"); + if (i) { + let e = i.getAttribute("href"); + !e || e.startsWith("#") || e.startsWith("javascript:") || i.getAttribute("target") || i.hostname !== location.hostname || n(); + } + }), document.addEventListener("submit", (e)=>{ + if (e.defaultPrevented) return; + let t = e.target; + if (!t) return; + let i = t.closest("form"); + i && !i.getAttribute("target") && n(); + }), new MutationObserver(()=>{ + Array.from(document.querySelectorAll(".spinning, .loading, #view > .spinning")).some((e)=>{ + let t = window.getComputedStyle(e); + return e.getClientRects().length > 0 && "none" !== t.display && "hidden" !== t.visibility && !e.closest(".btn"); + }) ? n() : i(); + }).observe(document.documentElement, { + childList: !0, + subtree: !0 + }); + } + function y(e, t) { + let i, n, r = t ? "fluent-sidebar-parent-slider" : "fluent-sidebar-slider", l = e.querySelector(`.${r}`); + l || ((l = document.createElement("div")).className = r, e.appendChild(l)); + let a = e.querySelector("li.active"); + if (!a || t && (a.classList.contains("slide") || a.querySelector(".slide-menu"))) { + l.style.height = "0px", l.style.opacity = "0"; + return; + } + l.style.opacity = "1"; + let s = t ? a.querySelector("a.menu, a.item") : a; + if (!s) return; + let o = s.getBoundingClientRect(), d = e.getBoundingClientRect(); + t ? (i = o.top - d.top + e.scrollTop + 0.2 * o.height, n = 0.6 * o.height) : (i = o.top - d.top + e.scrollTop + 0.15 * o.height, n = 0.7 * o.height), l.style.top = `${i}px`, l.style.height = `${n}px`; + } + function S() { + let e = document.querySelector("#mainmenu"); + if (!e) return; + let t = e.querySelector("ul.nav"); + if (t) { + let e = t.querySelector(".fluent-sidebar-parent-slider"); + if (e || ((e = document.createElement("div")).className = "fluent-sidebar-parent-slider", t.appendChild(e)), t.classList.add("has-slider"), !t.dataset.sliderInit && (t.dataset.sliderInit = "true", h && t.querySelectorAll("li > a.menu, li > a.item").forEach((e)=>{ + let i = e.getAttribute("href"); + i && "#" !== i && e.addEventListener("click", ()=>{ + try { + let e = t.querySelector(".fluent-sidebar-parent-slider"); + e && sessionStorage.setItem("fluent-sidebar-parent-pos", JSON.stringify({ + top: e.style.top, + height: e.style.height + })); + } catch {} + }); + })), h) { + let t = null; + try { + t = sessionStorage.getItem("fluent-sidebar-parent-pos"); + } catch {} + if (t) try { + let i = JSON.parse(t); + sessionStorage.removeItem("fluent-sidebar-parent-pos"), e.style.transition = "none", e.style.top = i.top, e.style.height = i.height, e.offsetHeight, e.style.transition = ""; + } catch {} + } + y(t, !0); + } + e.querySelectorAll("ul.slide-menu").forEach((e)=>{ + let t = e.classList.contains("active"), i = e.querySelector(".fluent-sidebar-slider"); + if (i || ((i = document.createElement("div")).className = "fluent-sidebar-slider", e.appendChild(i)), e.classList.add("has-slider"), !e.dataset.sliderInit && (e.dataset.sliderInit = "true", h && e.querySelectorAll("li > a").forEach((t)=>{ + let i = t.getAttribute("href"); + i && "#" !== i && t.addEventListener("click", ()=>{ + try { + let t = e.querySelector(".fluent-sidebar-slider"); + t && sessionStorage.setItem("fluent-sidebar-submenu-pos", JSON.stringify({ + top: t.style.top, + height: t.style.height + })); + } catch {} + }); + })), h && t) { + let e = null; + try { + e = sessionStorage.getItem("fluent-sidebar-submenu-pos"); + } catch {} + if (e) try { + let t = JSON.parse(e); + sessionStorage.removeItem("fluent-sidebar-submenu-pos"), i.style.transition = "none", i.style.top = t.top, i.style.height = t.height, i.offsetHeight, i.style.transition = ""; + } catch {} + } + y(e, !1); + }); + } + S(), new MutationObserver(()=>S()).observe(l, { + childList: !0, + subtree: !0 + }), document.addEventListener("fluent-menu-expand", ()=>{ + let e = document.querySelector("#mainmenu"); + if (e) { + let t = e.querySelector("ul.nav"); + t && (y(t, !0), setTimeout(()=>{ + y(t, !0); + }, 250)); + } + }), document.addEventListener("fluent-sidebar-state-change", ()=>{ + let e = document.querySelector("#mainmenu"); + if (e) { + let t = e.querySelector("ul.nav"); + t && y(t, !0), e.querySelectorAll("ul.slide-menu").forEach((e)=>{ + y(e, !1); + }); + } + }), window.addEventListener("resize", ()=>{ + let e = document.querySelector("#mainmenu"); + if (e) { + let t = e.querySelector("ul.nav"); + t && y(t, !0), e.querySelectorAll("ul.slide-menu").forEach((e)=>{ + y(e, !1); + }); + } + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/menu-features.ts + + + + + + + + + + +const setupMenuStartup = ()=>setupApplyChangePreview(); +const setupMenuFeatures = (r)=>{ + if (!r) { + setupTableWrappers(), setupSelectionPause(), setupErrorTooltips(), setupFluentSelects(), setupIfaceboxTooltips(), setupThemeFeatures(), setupMacSelector(), setupLogViewer(); + return; + } + setupMenuSearch(r); +}; + +;// CONCATENATED MODULE: ./web/resources/utils/sidebar-controller.ts +function applyDesktopSidebarState(e) { + document.body.setAttribute("data-sidebar-state", e), document.dispatchEvent(new CustomEvent("fluent-sidebar-state-change")); +} +function getDesktopSidebarState() { + try { + let e = localStorage.getItem("fluent-sidebar-state"); + return "collapsed" === e || "expanded" === e ? e : "expanded"; + } catch (e) { + return "expanded"; + } +} +function closeCollapsedPopups() { + document.querySelectorAll("#mainmenu ul.nav > li > a.menu.popup-open").forEach((e)=>{ + e.classList.remove("popup-open"); + }), document.querySelectorAll("#mainmenu ul.nav > li > ul.slide-menu.popup-open").forEach((e)=>{ + e.classList.remove("popup-open"), e.style.display = "none", e.style.top = ""; + }); +} +function adjustBrandTextSize() { + let e = document.querySelector(".sidenav-header .brand-text"); + if (e) { + let t = e.parentElement; + if (t) { + let a = t.clientWidth - 32; + if (a > 0) { + let t = 16; + for(e.style.fontSize = `${t}px`; e.scrollWidth > a && t > 9;)t -= 0.5, e.style.fontSize = `${t}px`; + } + } + } +} +function handleSidebarToggle(e) { + let t = document.querySelectorAll("a.showSide"), a = document.querySelector("#mainmenu"), d = document.querySelector(".darkMask"), o = document.querySelector(".main-right"); + 0 !== t.length && a && d && o && (Array.from(t).some((e)=>e.classList.contains("active")) ? (t.forEach((e)=>{ + e.classList.remove("active"); + }), a.classList.remove("active"), o.classList.remove("active"), d.classList.remove("active")) : (t.forEach((e)=>{ + e.classList.add("active"); + }), a.classList.add("active"), o.classList.add("active"), d.classList.add("active"), adjustBrandTextSize())); +} +function handleDesktopSidebarToggle(e) { + if (e.preventDefault(), e.stopPropagation(), window.innerWidth <= 768) return; + let t = "collapsed" == ("collapsed" === document.body.getAttribute("data-sidebar-state") ? "collapsed" : "expanded") ? "expanded" : "collapsed"; + closeCollapsedPopups(); + try { + localStorage.setItem("fluent-sidebar-state", t); + } catch (e) {} + applyDesktopSidebarState(t), "expanded" === t && adjustBrandTextSize(); +} +let sidebar_controller_e = !1; +function initSidebarController(t) { + if (sidebar_controller_e) return; + sidebar_controller_e = !0; + let a = document.querySelectorAll("a.showSide"), d = document.querySelector(".darkMask"), o = document.querySelector(".sidebar-collapse-toggle"), n = ui.createHandlerFn(t, "handleSidebarToggle") ?? (()=>{ + handleSidebarToggle(); + }), r = ui.createHandlerFn(t, "handleDesktopSidebarToggle") ?? ((e)=>{ + handleDesktopSidebarToggle(e); + }); + a.forEach((e)=>{ + e.addEventListener("click", n); + }), d && d.addEventListener("click", n), o && o.addEventListener("click", r), window.innerWidth > 768 ? applyDesktopSidebarState(getDesktopSidebarState()) : document.body.setAttribute("data-sidebar-state", "expanded"), window.addEventListener("resize", ()=>{ + adjustBrandTextSize(), window.innerWidth > 768 ? applyDesktopSidebarState(getDesktopSidebarState()) : document.body.setAttribute("data-sidebar-state", "expanded"); + }), document.addEventListener("click", (e)=>{ + if (window.innerWidth <= 768 || "collapsed" !== document.body.getAttribute("data-sidebar-state")) return; + let t = e.target, a = document.querySelector("#mainmenu"); + t && a?.contains(t) || closeCollapsedPopups(); + }); +} + +;// CONCATENATED MODULE: ./web/resources/utils/slide-animations.ts +let slide_animations_t = { + fast: 200, + normal: 400, + slow: 600 +}, slide_animations_e = (e)=>{ + if ("number" == typeof e) return e || slide_animations_t.normal; + switch(e){ + case "fast": + case "slow": + return slide_animations_t[e]; + default: + return slide_animations_t.normal; + } +}; +const SlideAnimations = { + durations: slide_animations_t, + runningAnimations: new WeakMap(), + slideDown (t, i, n) { + if (!t) return void console.warn("SlideAnimations.slideDown: No element provided"); + this.stop(t); + let s = slide_animations_e(i), o = { + display: t.style.display, + overflow: t.style.overflow, + height: t.style.height, + transition: t.style.transition + }; + t.style.display = "block", t.style.overflow = "hidden", t.style.height = "0px", t.style.transition = `height ${s}ms ease-out`, t.offsetHeight; + let l = t.scrollHeight; + t.style.height = `${l}px`; + let r = ()=>{ + if (t.style.height = o.height || "", t.style.overflow = o.overflow || "", t.style.transition = o.transition || "", this.runningAnimations.delete(t), n && "function" == typeof n) try { + n.call(t); + } catch (t) { + console.error("SlideAnimations callback error:", t); + } + }, a = setTimeout(r, s); + this.runningAnimations.set(t, { + timeoutId: a, + cleanup: r + }); + }, + slideUp (t, i, n) { + if (!t) return void console.warn("SlideAnimations.slideUp: No element provided"); + this.stop(t); + let s = slide_animations_e(i), o = { + display: t.style.display, + overflow: t.style.overflow, + height: t.style.height, + transition: t.style.transition + }, l = t.scrollHeight; + t.style.overflow = "hidden", t.style.height = `${l}px`, t.style.transition = `height ${s}ms ease-out`, t.offsetHeight, t.style.height = "0px"; + let r = ()=>{ + if (t.style.display = "none", t.style.height = o.height || "", t.style.overflow = o.overflow || "", t.style.transition = o.transition || "", this.runningAnimations.delete(t), n && "function" == typeof n) try { + n.call(t); + } catch (t) { + console.error("SlideAnimations callback error:", t); + } + }, a = setTimeout(r, s); + this.runningAnimations.set(t, { + timeoutId: a, + cleanup: r + }); + }, + stop (t) { + if (!t) return; + let e = this.runningAnimations.get(t); + e && (clearTimeout(e.timeoutId), e.cleanup()), t.style.transition = "", t.offsetHeight; + }, + isAnimating (t) { + return this.runningAnimations.has(t); + } +}; + +;// CONCATENATED MODULE: ./web/resources/menu-fluent.tsx + + + + + + +let menu_fluent_v = !1; +function menu_fluent_y(e) { + menu_fluent_v || (menu_fluent_v = !0, setupMenuFeatures()), setupMenuFeatures(e); +} +const main = baseclass.extend({ + async __init__ () { + setupMenuStartup(), initSidebarController(this); + let e = getResolvedMenuLayout(), t = getCachedMenu(), l = !1; + if (t) { + let i = buildMenuPresentation(t.tree, e ?? null); + this.render(t.tree, i), menu_fluent_y(i), l = !0; + } + try { + let i = void 0 === e, [a] = await Promise.all([ + ui.menu.load(), + i ? L.uci.load("fluent") : Promise.resolve() + ]), s = saveMenuCache(a); + if (!l || s !== t?.raw) { + if (i) { + let t = L.uci.get_first("fluent", "global", "menu_layout"); + e = "string" == typeof t || Array.isArray(t) ? t : null; + } + let t = buildMenuPresentation(a, e ?? null); + this.render(a, t), menu_fluent_y(t); + } + } catch (e) { + l || console.error("Fluent menu: Failed to load menu data", e); + } + }, + render (e, t) { + let l = e, n = ""; + if (t.configured) this.renderConfiguredMenu(t); + else { + let t = ui.menu.getChildren(e); + for(let e = 0; e < t.length; e++)(L.env.requestpath.length ? t[e].name === L.env.requestpath[0] : 0 === e) && this.renderMainMenu(t[e], t[e].name); + } + if (L.env.dispatchpath.length >= 3) { + for(let e = 0; e < 3 && l; e++){ + let t = L.env.dispatchpath[e]; + l = l.children?.[t], n = n + (n ? "/" : "") + t; + } + l && this.renderTabMenu(l, n, void 0, t.hiddenPaths); + } else { + let e = document.querySelector("#tabmenu"); + e && (e.innerHTML = "", e.style.display = "none"); + } + saveRenderedHtmlCache(); + }, + handleMenuExpand (e) { + let t = e.currentTarget; + if (!t) return; + let l = t.parentNode, n = t.nextElementSibling, i = window.innerWidth > 768 && "collapsed" === document.body.getAttribute("data-sidebar-state"), a = !1; + if (document.querySelectorAll(i ? ".main .main-left .nav > li > ul.slide-menu.popup-open" : ".main .main-left .nav > li > ul.slide-menu.active").forEach((e)=>{ + a || e !== n || (a = !0), e.classList.remove("popup-open", "active"), e.previousElementSibling?.classList.remove("popup-open", "active"), SlideAnimations.stop(e), i ? (e.style.display = "none", e.style.top = "") : SlideAnimations.slideUp(e, "fast"); + }), n) { + if (!a) { + let e = l?.querySelector(".slide-menu"); + if (e) { + if (n.classList.add(i ? "popup-open" : "active"), t.classList.add(i ? "popup-open" : "active"), i) { + SlideAnimations.stop(e), e.style.display = "block"; + let l = t.getBoundingClientRect(), n = e.offsetHeight, i = Math.max(8, window.innerHeight - n - 8), a = l.top - 8; + e.style.top = `${Math.min(i, Math.max(8, a))}px`; + } else e.style.top = "", SlideAnimations.slideDown(e, "fast"); + e.querySelectorAll("li > a").forEach((e)=>{ + e.addEventListener("click", ()=>{ + closeCollapsedPopups(); + }, { + once: !0 + }); + }); + } + t.blur(); + } + document.dispatchEvent(new CustomEvent("fluent-menu-expand")), e.preventDefault(), e.stopPropagation(); + } + }, + renderMainMenu (n, i, a) { + let r = (a || 0) + 1, s = a && n.title ? n.title.replace(/ /g, "_") : void 0, d = jsx("ul", { + class: a ? "slide-menu" : "nav", + "data-parent": s || void 0 + }), u = ui.menu.getChildren(n); + if (0 === u.length || r > 2) return jsx(Fragment, {}); + for(let l = 0; l < u.length; l++){ + let a = u[l], s = L.env.dispatchpath[r] === a.name && L.env.dispatchpath[r - 1] === n.name, c = this.renderMainMenu(a, `${i}/${a.name}`, r), o = c.children.length > 0, p = o ? "slide" : null, h = o ? "menu" : "item"; + s && (d.classList.add("active"), p = p ? `${p} active` : "null active"); + let m = s ? `${h} active` : h, f = jsxs("li", { + class: p ?? void 0, + children: [ + jsxs("a", { + href: L.url(i, a.name), + onclick: 1 === r ? ui.createHandlerFn(this, "handleMenuExpand") : null, + class: m, + "data-title": (a.title || "").replace(/ /g, "_"), + children: [ + 1 === r || 2 === r ? jsx("span", { + class: "menu-icon" + }) : null, + jsx("span", { + class: "menu-label", + children: _(a.title || "") + }) + ] + }), + c + ] + }); + d.appendChild(f); + } + if (1 === r) { + let e = document.querySelector("#mainmenu"); + if (e) { + let t = "function" == typeof e.querySelector ? e.querySelector("ul.nav") : null; + t && "function" == typeof t.replaceWith ? t.replaceWith(d) : e.appendChild(d), e.style.display = "", adjustBrandTextSize(); + } + } + return d; + }, + renderConfiguredMenu (l) { + let n = jsx("ul", { + class: "nav" + }); + for (let i of l.categories){ + if (l.hiddenCategoryIds.has(i.id)) continue; + let a = i.items.filter((e)=>!l.hiddenPaths.has(e.path)); + if (!i.primary && 0 === a.length) continue; + let r = jsx("ul", { + class: "slide-menu", + "data-parent": i.title.replace(/ /g, "_") + }), s = !1; + for (let l of a){ + let n = l.pathSegments.every((e, t)=>L.env.dispatchpath[t] === e); + s ||= n, r.appendChild(jsx("li", { + class: n ? "active" : void 0, + children: jsxs("a", { + href: L.url(l.path), + class: `item${n ? " active" : ""}`, + "data-title": l.rawTitle.replace(/ /g, "_"), + children: [ + jsx("span", { + class: "menu-icon" + }), + jsx("span", { + class: "menu-label", + children: l.title + }) + ] + }) + })); + } + s && r.classList.add("active"), i.primary && L.env.dispatchpath.length <= 2 && (s ||= i.primary.pathSegments.every((e, t)=>L.env.dispatchpath[t] === e)); + let d = a.length > 0, u = i.primary?.path ?? a[0]?.path ?? "#", c = i.primary?.rawTitle ?? i.title, o = `${d ? "slide" : ""}${s ? " active" : ""}`.trim() || void 0; + n.appendChild(jsxs("li", { + class: o, + children: [ + jsxs("a", { + href: "#" === u ? u : L.url(u), + onclick: d ? ui.createHandlerFn(this, "handleMenuExpand") : null, + class: `${d ? "menu" : "item"}${s ? " active" : ""}`, + "data-title": c.replace(/ /g, "_"), + children: [ + jsx("span", { + class: "menu-icon" + }), + jsx("span", { + class: "menu-label", + children: i.title + }) + ] + }), + d ? r : null + ] + })); + } + let i = document.querySelector("#mainmenu"); + if (i) { + let e = "function" == typeof i.querySelector ? i.querySelector("ul.nav") : null; + e && "function" == typeof e.replaceWith ? e.replaceWith(n) : i.appendChild(n), i.style.display = "", adjustBrandTextSize(); + } + return n; + }, + renderTabMenu (t, n, i, a) { + let r = L.env.dispatchpath.slice(0, 2).join("/"), s = L.env.dispatchpath.slice(0, 3).join("/"); + if (a.has(r) || a.has(s)) return jsx(Fragment, {}); + let d = document.querySelector("#tabmenu"), u = (i || 0) + 1, c = jsx("ul", { + class: "tabs" + }), o = ui.menu.getChildren(t), p = null; + if (0 === o.length) return d && 1 === u && (d.innerHTML = "", d.style.display = "none"), jsx(Fragment, {}); + for(let t = 0; t < o.length; t++){ + let l = o[t], i = L.env.dispatchpath[u + 2] === l.name, a = i ? " active" : "", r = jsx("li", { + class: `tabmenu-item-${l.name}${a}`, + children: jsx("a", { + href: L.url(n, l.name), + children: _(l.title || "") + }) + }); + c.appendChild(r), i && (p = l); + } + if (d && (1 === u && (d.innerHTML = ""), d.appendChild(c), d.style.display = "", p)) { + let e = this.renderTabMenu(p, `${n}/${p.name}`, u, a); + e.children.length > 0 && d.appendChild(e); + } + return c; + }, + adjustBrandTextSize () { + adjustBrandTextSize(); + }, + handleSidebarToggle (e) { + handleSidebarToggle(e); + }, + handleDesktopSidebarToggle (e) { + handleDesktopSidebarToggle(e); + } +}); + + +return main; \ No newline at end of file diff --git a/luci-theme-fluent/htdocs/luci-static/resources/view/fluent-config.js b/luci-theme-fluent/htdocs/luci-static/resources/view/fluent-config.js new file mode 100644 index 000000000..e9b825169 --- /dev/null +++ b/luci-theme-fluent/htdocs/luci-static/resources/view/fluent-config.js @@ -0,0 +1,2246 @@ +"use strict"; +"require baseclass"; +"require ui"; +"require dom"; +// WARN: generated by rsbuild - do not edit + +// The require scope +var __webpack_require__ = {}; + +// webpack/runtime/define_property_getters +(() => { +__webpack_require__.d = (exports, getters, values) => { + var define = (defs, kind) => { + for(var key in defs) { + if(__webpack_require__.o(defs, key) && !__webpack_require__.o(exports, key)) { + Object.defineProperty(exports, key, { enumerable: true, [kind]: defs[key] }); + } + } + }; + define(getters, "get"); + define(values, "value"); +}; +})(); +// webpack/runtime/has_own_property +(() => { +__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +})(); +// webpack/runtime/make_namespace_object +(() => { +// define __esModule on exports +__webpack_require__.r = (exports) => { + if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { + Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + } + Object.defineProperty(exports, '__esModule', { value: true }); +}; +})(); +var __webpack_exports__ = {}; +// ESM COMPAT FLAG +__webpack_require__.r(__webpack_exports__); + +// EXPORTS +__webpack_require__.d(__webpack_exports__, { + main: () => (/* binding */ main) +}); + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-factory.ts +const Fragment = Symbol.for("jsx.fragment"); +function jsx_factory_e(e, t) { + let { children: n, ...r } = null === t || "object" != typeof t || Array.isArray(t) ? {} : t, o = function e(t, n = []) { + for (let r of t)null != r && "boolean" != typeof r && (Array.isArray(r) ? e(r, n) : n.push(r instanceof Node ? r : String(r))); + return n; + }(null == n ? [] : Array.isArray(n) ? n : [ + n + ]); + if (e === Fragment) { + let e = document.createDocumentFragment(); + return e.append(...o), e; + } + if ("function" == typeof e) { + let t = Reflect.apply(e, void 0, [ + { + ...r, + children: o + } + ]); + if (!(t instanceof Node)) throw TypeError("JSX components must return a DOM Node"); + return t; + } + if ("string" != typeof e) throw TypeError("JSX element types must be tag names or component functions"); + let f = {}, l = { + ...r + }; + for (let [e, t] of Object.entries(l))e.startsWith("on") && "function" == typeof t ? (f[e] = t, delete l[e]) : "boolean" == typeof t && (t ? l[e] = e : delete l[e]); + let s = Object.keys(l).length > 0 ? o.length > 1 ? E(e, l, o) : E(e, l, o[0]) : o.length > 1 ? E(e, {}, o) : E(e, {}, o[0]); + for (let [e, t] of Object.entries(f)){ + let n = e.slice(2).toLowerCase(); + s.addEventListener(n, t); + } + return s; +} +function jsx(t, n) { + return jsx_factory_e(t, n); +} +function jsxs(t, n) { + return jsx_factory_e(t, n); +} +function jsxDEV(t, n) { + return jsx_factory_e(t, n); +} + +;// CONCATENATED MODULE: ../node_modules/.pnpm/@lazulikao+luci-types@https_685a81b9ead15f36368ce6e6ebcf4a27/node_modules/@lazulikao/luci-types/src/jsx/jsx-runtime.ts + + +;// CONCATENATED MODULE: ./web/resources/utils/update.ts +let update_t = L.rpc; +const callGetVersion = update_t.declare({ + object: "luci.fluent", + method: "get_version" +}); +const callStartDownload = update_t.declare({ + object: "luci.fluent", + method: "start_download", + params: [ + "url", + "i18n_url", + "i18n_urls" + ] +}); +const callCheckDownload = update_t.declare({ + object: "luci.fluent", + method: "check_download" +}); +const callDoInstall = update_t.declare({ + object: "luci.fluent", + method: "do_install", + params: [ + "hash", + "i18n_hash", + "i18n_hashes" + ] +}); +class GitHubAPIError extends Error { + status; + constructor(t, e){ + super(t), this.name = "GitHubAPIError", this.status = e; + } +} +function matchI18nAsset(t, e, l) { + let s = e.toLowerCase().trim(); + for (let e of t){ + let t = String(e.name || "").toLowerCase(); + if ("apk" === l) { + if (t.startsWith(`luci-i18n-fluent-${s}-`) && t.endsWith(".apk")) return e; + } else if ((t.startsWith(`luci-i18n-fluent-${s}_`) || t.startsWith(`luci-i18n-fluent-${s}-`)) && t.endsWith(".ipk")) return e; + } + return null; +} +async function fetchLatestRelease(t, e, l = [], s) { + let a = {}; + s && (a.Authorization = `token ${s}`); + let r = await fetch("nightly" === t ? "https://api.github.com/repos/LazuliKao/luci-theme-fluent/releases/tags/nightly" : "https://api.github.com/repos/LazuliKao/luci-theme-fluent/releases/latest", { + headers: a + }), n = await r.json(); + if (!r.ok) { + let t = n?.message ? `: ${n.message}` : ""; + throw new GitHubAPIError(`GitHub API returned status ${r.status}${t}`, r.status); + } + let i = n.assets || [], o = null, u = []; + for (let t of i){ + let l = String(t.name || ""); + "apk" === e && /^luci-theme-fluent-[0-9].*\.apk$/.test(l) ? o = t : "ipk" === e && /^luci-theme-fluent_[^/]+\.ipk$/.test(l) && (o = t); + } + for (let t of l){ + let l = matchI18nAsset(i, t, e); + l && !u.includes(l) && u.push(l); + } + return { + tag_name: String(n.tag_name || ""), + published_at: String(n.published_at || ""), + body: String(n.body || ""), + html_url: String(n.html_url || ""), + package_asset: o, + i18n_assets: u + }; +} + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/about.tsx + +let about_l = L.form, about_n = "https://ghfast.top/"; + +let about_u = about_l.DummyValue.extend({ + renderWidget: (l, u, p)=>{ + let h = "1.0.1", b = "ipk", f = [], g = jsxs("div", { + class: "fluent-about-logo", + children: [ + jsx("img", { + src: `${L.media()}/img/fluent.svg`, + alt: "Fluent Theme Logo" + }), + jsx("h2", { + children: "Fluent Theme" + }), + jsx("p", { + class: "fluent-about-subtitle", + children: _("Fluent Design 2 theme for LuCI") + }) + ] + }), y = jsxs("div", { + class: "fluent-about-details", + children: [ + jsxs("div", { + class: "fluent-about-detail-row", + children: [ + jsxs("strong", { + children: [ + _("Author"), + ":" + ] + }), + jsx("span", { + children: "LazuliKao" + }) + ] + }), + jsxs("div", { + class: "fluent-about-detail-row", + children: [ + jsxs("strong", { + children: [ + _("Installed version"), + ":" + ] + }), + jsx("span", { + class: "fluent-about-current-version", + children: "v..." + }) + ] + }), + jsxs("div", { + class: "fluent-about-detail-row", + children: [ + jsxs("strong", { + children: [ + _("Package format"), + ":" + ] + }), + jsx("span", { + class: "fluent-about-pkg-type", + children: "..." + }) + ] + }), + jsxs("div", { + class: "fluent-about-detail-row", + children: [ + jsxs("strong", { + children: [ + _("Source code"), + ":" + ] + }), + jsx("span", { + children: jsx("a", { + href: "https://github.com/LazuliKao/luci-theme-fluent", + target: "_blank", + rel: "noreferrer", + children: "GitHub Repository" + }) + }) + ] + }) + ] + }), m = jsxs("select", { + class: "cbi-input-select", + id: "update-channel-select", + children: [ + jsx("option", { + value: "stable", + children: _("Stable Channel") + }), + jsx("option", { + value: "nightly", + children: _("Nightly Channel (Prerelease)") + }) + ] + }), w = jsxs("select", { + class: "cbi-input-select", + id: "update-method-select", + children: [ + jsx("option", { + value: "backend_official", + children: _("Backend (Official GitHub)") + }), + jsx("option", { + value: "backend_ghproxy", + children: _("Backend (GHProxy Acceleration)") + }) + ] + }), v = jsx("button", { + class: "btn cbi-button cbi-button-action", + type: "button", + children: _("Check for updates") + }), k = jsxs("div", { + class: "fluent-update-controls", + style: "display: flex; gap: 15px; align-items: center; flex-wrap: wrap;", + children: [ + jsxs("div", { + class: "fluent-update-channel-selector", + children: [ + jsxs("label", { + htmlFor: "update-channel-select", + children: [ + _("Update channel"), + ": " + ] + }), + m + ] + }), + v + ] + }), x = jsx("div", { + class: "fluent-update-status", + style: "display: none" + }), T = jsx("div", { + class: "fluent-progress-bar__fill", + style: "width: 0%" + }), C = jsx("div", { + class: "fluent-progress-bar", + style: "display: none", + children: T + }), S = jsx("div", { + class: "fluent-progress-text", + style: "display: none" + }), $ = jsx("div", { + class: "fluent-update-card", + style: "display: none" + }), F = jsxs("div", { + class: "fluent-about-manager", + children: [ + g, + y, + jsx("hr", { + class: "fluent-about-divider" + }), + jsx("h3", { + children: _("Software Update") + }), + k, + x, + $, + C, + S + ] + }), P = (e, t = "info")=>{ + dom.content(x, [ + document.createTextNode(e) + ]), x.className = `fluent-update-status status-${t}`, x.style.display = "block"; + }, A = (e, t, a)=>{ + C.style.display = "block", S.style.display = "block", T.style.width = `${t}%`, T.className = `fluent-progress-bar__fill fill-${e}`, dom.content(S, [ + document.createTextNode(`${a} (${t}%)`) + ]); + }, z = ()=>{ + C.style.display = "none", S.style.display = "none"; + }; + (async ()=>{ + try { + let e = await callGetVersion(); + h = e.version, b = e.pkg_type, f = e.installed_i18n || (e.i18n_zh_cn_installed ? [ + "zh-cn" + ] : []); + let t = y.querySelector(".fluent-about-current-version"); + t && (t.textContent = `v${h}`); + let a = y.querySelector(".fluent-about-pkg-type"); + a && (a.textContent = "apk" === b ? "APK (OpenWrt 25.12+)" : "IPK (OpenWrt 24.10)"); + } catch (e) { + console.error("Failed to fetch version", e), P(_("Failed to fetch current theme version."), "error"); + } + })(); + let N = async (l)=>{ + let s = m.value; + try { + let c = await fetchLatestRelease(s, b, f, l); + console.log(c), v.disabled = !1; + let u = h.replace(/^v/, "").trim(), p = c.tag_name.replace(/^v/, "").trim(), g = L.naturalCompare(p, u) > 0, y = !g && "nightly" !== s; + if (y ? P(_("Your theme is up to date!"), "success") : P(g ? _("A new version is available!") : _("Nightly build available (reinstallation check)."), g ? "warn" : "info"), !c.package_asset) return void P(_("No matching package asset found for your system architecture in this release."), "error"); + let k = c.body ? c.body : "", x = jsx("button", { + class: "btn cbi-button cbi-button-save", + type: "button", + style: "white-space: nowrap;", + children: _("Download and Install") + }), T = [ + jsxs("div", { + class: "fluent-update-header", + children: [ + jsx("span", { + class: "fluent-update-badge", + children: "nightly" === p ? "Nightly" : `v${p}` + }), + jsx("span", { + class: "fluent-update-date", + children: c.published_at.split("T")[0] + }) + ] + }) + ]; + k && T.push(jsx("pre", { + class: "fluent-update-changelog", + children: k + })), y || T.push(jsxs("div", { + class: "fluent-update-footer", + style: "display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 15px; flex-wrap: wrap;", + children: [ + w, + x + ] + })), dom.content($, T), $.style.display = "block", x.addEventListener("click", async ()=>{ + let l = c.package_asset; + if (!l) return void P(_("No matching package asset found for your system architecture in this release."), "error"); + x.disabled = !0, m.disabled = !0, w.disabled = !0, v.disabled = !0, P(_("Starting update process..."), "info"); + let s = c.i18n_assets || [], d = null, u = []; + for (let e of (l.digest?.startsWith("sha256:") ? d = l.digest.replace("sha256:", "") : (console.warn("Unable to determine expected package hash from digest. Skipping verification."), d = "skip"), s))e.digest?.startsWith("sha256:") ? u.push(e.digest.replace("sha256:", "")) : u.push("skip"); + let p = async ()=>{ + let c = w.value.includes("ghproxy"); + P(_("Starting backend download..."), "info"), A("download", 0, _("Downloading on router")); + let p = c ? about_n + l.browser_download_url : l.browser_download_url, h = s.map((e)=>c ? about_n + e.browser_download_url : e.browser_download_url).join(" "), b = await callStartDownload(p, h.split(" ")[0] || "", h); + if (0 !== b.result) throw Error(b.message || "Failed to start router download."); + for(;;){ + let e = await callCheckDownload(), t = e.size || 0, a = s.reduce((e, t)=>e + (t.size || 0), 0), n = l.size + a, i = n > 0 ? Math.min(Math.round(t / n * 100), 100) : 0; + if (A("download", i, `${_("Downloading on router")} (${(t / 1024).toFixed(0)} / ${(n / 1024).toFixed(0)} KB)`), !e.running) if (0 !== e.code) throw Error("Router background download failed or file is empty."); + else break; + await new Promise((e)=>setTimeout(e, 1000)); + } + if (!await new Promise((l)=>{ + L.ui.showModal(_("Confirm Installation"), jsxs(Fragment, { + children: [ + jsx("div", { + children: jsx("p", { + children: _("The theme package is ready. Are you sure you want to proceed with the installation?") + }) + }), + jsxs("div", { + class: "right", + children: [ + jsx("button", { + type: "button", + class: "btn", + onclick: ()=>{ + L.ui.hideModal(), l(!1); + }, + children: _("Cancel") + }), + jsx("button", { + type: "button", + class: "btn cbi-button-action", + onclick: ()=>{ + L.ui.hideModal(), l(!0); + }, + children: _("Continue") + }) + ] + }) + ] + })); + })) throw Error(_("Installation cancelled by user.")); + P(_("Triggering installation on router..."), "info"), A("install", 100, _("Installing package")); + let f = await callDoInstall(d, u[0] || "", u.join(" ")); + if (0 !== f.result) throw Error(f.message || "Router installation failed."); + let g = jsx("pre", { + style: "background: var(--fluent-code-bg, #1a1a1a); color: var(--fluent-text, #fff); padding: 10px; border-radius: 4px; font-family: monospace; font-size: 12px; white-space: pre-wrap; word-wrap: break-word; max-height: 200px; overflow-y: auto; margin-top: 15px;" + }); + (async ()=>{ + let e = L.env.ubuspath || "/ubus", t = L.env.sessionid; + for(;;)try { + let a = await fetch(e, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: 1, + method: "call", + params: [ + t, + "luci.fluent", + "get_install_log", + {} + ] + }) + }), l = await a.json(); + if (l.error) break; + l.result?.[1]?.log && (g.textContent = l.result[1].log, g.scrollTop = g.scrollHeight), await new Promise((e)=>setTimeout(e, 1000)); + } catch { + break; + } + })(), P(_("Theme successfully updated! Reloading RPC service, please refresh the page in 5 seconds."), "success"), A("done", 100, _("Finished")), dom.content($, [ + jsxs("div", { + class: "fluent-update-success", + children: [ + jsx("h3", { + style: "color: var(--fluent-success); text-align: center;", + children: _("Upgrade Successful!") + }), + jsx("p", { + style: "text-align: center; margin: 15px 0;", + children: _("The theme is being installed on your router. The logs are displayed below. Reloading the web interface will apply the changes once RPCD restarts.") + }), + g, + jsx("div", { + style: "display: flex; justify-content: center; margin-top: 20px;", + children: jsx("button", { + class: "btn cbi-button-action", + type: "button", + style: "margin-top: 15px", + onclick: ()=>{ + window.location.reload(); + }, + children: _("Reload Web Interface") + }) + }) + ] + }) + ]); + }; + try { + await p(); + } catch (e) { + console.error("Update failed", e), P(`${_("Update failed")}: ${e instanceof Error ? e.message : String(e)}`, "error"), x.disabled = !1, x.removeAttribute("disabled"), m.disabled = !1, m.removeAttribute("disabled"), w.disabled = !1, w.removeAttribute("disabled"), v.disabled = !1, v.removeAttribute("disabled"), z(); + } + }); + } catch (l) { + if (v.disabled = !1, console.error("Failed checking updates", l), l instanceof GitHubAPIError && 403 === l.status) { + let n = jsx("input", { + type: "text", + class: "cbi-input-text", + style: "width: 100%", + placeholder: "ghp_..." + }), r = jsx("p", { + children: _("API rate limit exceeded. Please enter a GitHub Token to continue. This token is only used for frontend requests and will not be saved on the router backend.") + }), i = jsxs("p", { + style: "margin-top: 10px; font-size: 13px; color: var(--fluent-text-secondary);", + children: [ + _("You can create a new token at"), + " ", + jsx("a", { + href: "https://github.com/settings/personal-access-tokens", + target: "_blank", + rel: "noopener noreferrer", + children: "https://github.com/settings/personal-access-tokens/new" + }), + ".", + _("The token does NOT require any permissions/scopes to be granted (read-only public access is sufficient).") + ] + }), s = jsx("pre", { + style: "margin-top: 10px; margin-bottom: 15px; font-size: 12px; white-space: pre-wrap; word-break: break-word; color: var(--fluent-error-text); background: var(--fluent-card-bg); padding: 8px; border-radius: var(--fluent-border-radius);", + children: l instanceof Error ? l.message : String(l) + }); + L.ui.showModal(_("GitHub Token Required"), jsxs(Fragment, { + children: [ + r, + i, + s, + n, + jsxs("div", { + class: "right", + children: [ + jsx("button", { + type: "button", + class: "btn", + onclick: ()=>L.ui.hideModal(), + children: _("Cancel") + }), + jsx("button", { + type: "button", + class: "btn cbi-button-save", + onclick: ()=>{ + let e = n.value.trim(); + L.ui.hideModal(), e && (P(_("Retrying with token..."), "info"), v.disabled = !0, N(e)); + }, + children: _("Submit") + }) + ] + }) + ] + })); + } else P(`${_("Failed to check for updates")}: ${l instanceof Error ? l.message : String(l)}`, "error"); + } + }; + return v.addEventListener("click", ()=>{ + P(_("Checking for updates..."), "info"), v.disabled = !0, $.style.display = "none", z(), N(); + }), F; + } +}); +const registerAboutTab = (e)=>{ + e.tab("about", _("About")), e.taboption("about", about_u, "_about_mgr"); +}; + +;// CONCATENATED MODULE: ./web/resources/fluent-defaults.ts +const FLUENT_DEFAULTS = { + mode: "auto", + primary: "#0078d4", + dark_primary: "#4da6ff", + progressbar_font: "#2e2b60", + dark_progressbar_font: "#d6d9e5", + page_bg: "#fafafa", + card_bg: "#ffffff", + sidebar_bg: "#f3f3f3", + dark_page_bg: "#1b1b1b", + dark_card_bg: "#2d2d2d", + dark_sidebar_bg: "#1f1f1f", + font_weight: "normal", + font_size: "14", + border_radius: "4", + control_height: "32", + sidebar_width: "260", + header_height: "48", + login_bg: "microsoft", + blur: "40", + transparency: "0.8", + blur_dark: "20", + transparency_dark: "0.8", + card_shadow: "small", + transition_speed: "normal", + custom_css: "", + custom_select: "1", + view_transition: "1", + tab_animation: "1", + prefers_reduced_motion: "1", + loading_bar: "1", + sticky_action_bar: "1", + uci_changes_preview: "1", + direction_mode: "auto", + progressbar_text_position: "top-start" +}; +const fluentFlagDefault = (r)=>"1" === r; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/shared.ts +let shared_e = L.form; +const omitDefaultValue = (e)=>{ + let t = e.parse.bind(e); + e.parse = (r)=>e.isActive(r) && e.formvalue(r) === e.default ? e.isValid(r) ? (e.remove(r), Promise.resolve()) : t(r) : t(r); +}; +const transparencySteps = [ + 0, + 0.1, + 0.2, + 0.3, + 0.4, + 0.5, + 0.6, + 0.7, + 0.8, + 0.9, + 1 +]; +let shared_t = "fluent-live-preview", shared_r = /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i, shared_a = { + primary: { + cssVar: "--fluent-primary", + isDark: !1 + }, + dark_primary: { + cssVar: "--fluent-primary", + isDark: !0 + }, + page_bg: { + cssVar: "--fluent-bg", + isDark: !1 + }, + dark_page_bg: { + cssVar: "--fluent-bg", + isDark: !0 + }, + card_bg: { + cssVar: "--fluent-bg-card", + isDark: !1 + }, + dark_card_bg: { + cssVar: "--fluent-bg-card", + isDark: !0 + }, + sidebar_bg: { + cssVar: "--fluent-sidebar-bg", + isDark: !1 + }, + dark_sidebar_bg: { + cssVar: "--fluent-sidebar-bg", + isDark: !0 + }, + progressbar_font: { + cssVar: "--fluent-progressbar-font-color", + isDark: !1 + }, + dark_progressbar_font: { + cssVar: "--fluent-progressbar-font-color", + isDark: !0 + } +}, shared_l = new globalThis.Map(); +const configureHexColorValue = (s, o, n = !1)=>{ + s.validate = (e, t)=>!e || shared_r.test(String(t)) || _("Expecting: %s").format(_("valid HEX color value")), s.render = (i, c, u)=>{ + let d = shared_e.Value.prototype.render.call(s, i, c, u), p = ()=>{ + let e = document.querySelector(`[id^="widget.cbid.fluent."][id$=".${o}"]`); + e && ((e, t)=>{ + if ("true" === e.dataset.fluentColorPicker) return; + let a = e.parentElement; + if (!a) return; + e.dataset.fluentColorPicker = "true", e.classList.add("fluent-color-field__text"); + let l = document.createElement("div"); + l.className = "fluent-color-field"; + let s = document.createElement("label"); + s.className = "fluent-color-swatch", s.title = _("Choose color"); + let o = document.createElement("input"); + o.type = "color", o.className = "fluent-color-swatch__input", o.setAttribute("aria-label", _("Choose color")); + let n = document.createElement("span"); + n.className = "fluent-color-swatch__preview"; + let i = (e)=>{ + shared_r.test(e) && (o.value = e, n.style.backgroundColor = e); + }; + i(e.value), o.addEventListener("input", ()=>{ + e.value = o.value, n.style.backgroundColor = o.value, shared_r.test(o.value) && t(o.value); + }), e.addEventListener("input", ()=>{ + i(e.value), shared_r.test(e.value) && t(e.value); + }), s.appendChild(o), s.appendChild(n), a.insertBefore(l, e), l.appendChild(e), l.appendChild(s); + })(e, (e)=>((e, r)=>{ + let s = shared_a[e]; + if (!s) return; + let o = `${s.isDark ? "dark" : "light"}|${s.cssVar}`; + shared_l.set(o, { + selector: s.isDark ? ':root[data-theme="dark"]' : ":root", + cssVar: s.cssVar, + value: r + }), (()=>{ + let e, r = ((e = document.getElementById(shared_t)) || ((e = document.createElement("style")).id = shared_t, document.head.appendChild(e)), e), a = new globalThis.Map(); + for (let e of shared_l.values()){ + let t = a.get(e.selector) ?? []; + t.push(`${e.cssVar}: ${e.value};`), a.set(e.selector, t); + } + let s = ""; + for (let [e, t] of a)s += `${e} { ${t.join(" ")} }\n`; + r.textContent = s; + })(); + })(o, e)); + }; + return n ? requestAnimationFrame(p) : setTimeout(p, 0), d; + }; +}; +const createModeSubtabs = (t, r, a)=>{ + let l = t.taboption(r, shared_e.SectionValue, a, shared_e.TypedSection, "global").subsection; + return l.anonymous = !0, l.addremove = !1, l.tab("light", _("Light mode")), l.tab("dark", _("Dark mode")), l; +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/advanced.ts +let advanced_e = L.form; + + +const registerAdvancedTab = (d)=>{ + d.tab("advanced", _("Advanced"), _("Adjust layout, typography, transition timing, shadows, and inject custom CSS variables or rules when the built-in controls are not enough.")); + { + let o = d.taboption("advanced", advanced_e.Value, "font_size", _("Base font size"), _("Sets the base interface font size in pixels. Most theme text scales from this value through the Fluent CSS variables. Recommended range: 12-18px.")); + o.datatype = "range(12,18)", o.default = FLUENT_DEFAULTS.font_size, omitDefaultValue(o), o.placeholder = FLUENT_DEFAULTS.font_size; + } + { + let o = d.taboption("advanced", advanced_e.Value, "sidebar_width", _("Sidebar width"), _("Width of the main navigation sidebar in pixels.")); + o.datatype = "range(200,420)", o.default = FLUENT_DEFAULTS.sidebar_width, omitDefaultValue(o), o.placeholder = FLUENT_DEFAULTS.sidebar_width; + } + { + let o = d.taboption("advanced", advanced_e.Value, "header_height", _("Header height"), _("Height of the top header bar in pixels.")); + o.datatype = "range(40,96)", o.default = FLUENT_DEFAULTS.header_height, omitDefaultValue(o), o.placeholder = FLUENT_DEFAULTS.header_height; + } + { + let o = d.taboption("advanced", advanced_e.ListValue, "border_radius", _("Corner radius"), _("Controls the shared Fluent corner radius tokens used by cards, buttons, inputs, and related UI surfaces.")); + o.value("0", _("Square (0px)")), o.value("2", _("Small (2px)")), o.value("4", _("Medium (4px)")), o.value("6", _("Rounded (6px)")), o.value("8", _("Large (8px)")), o.value("12", _("Extra large (12px)")), o.default = FLUENT_DEFAULTS.border_radius, omitDefaultValue(o); + } + { + let o = d.taboption("advanced", advanced_e.ListValue, "card_shadow", _("Card shadow"), _("Select the shadow depth applied to themed cards and panels.")); + o.value("none", _("None")), o.value("small", _("Small")), o.value("medium", _("Medium")), o.value("large", _("Large")), o.default = FLUENT_DEFAULTS.card_shadow, omitDefaultValue(o); + } + { + let o = d.taboption("advanced", advanced_e.ListValue, "transition_speed", _("Theme transition speed"), _("Controls the shared Fluent transition timing used by menu, header, and other theme animations.")); + o.value("fast", _("Fast")), o.value("normal", _("Normal")), o.value("slow", _("Slow")), o.value("none", _("Disabled")), o.default = FLUENT_DEFAULTS.transition_speed, omitDefaultValue(o); + } + { + let o = d.taboption("advanced", advanced_e.TextValue, "custom_css", _("Custom CSS"), _("Optional raw CSS injected into the Fluent header template. Use this for extra --fluent-* variable overrides or page-specific tweaks that are not exposed as dedicated options.")); + o.default = FLUENT_DEFAULTS.custom_css, omitDefaultValue(o), o.rmempty = !0, o.rows = 12, o.wrap = "off", o.placeholder = ":root {\n --fluent-sidebar-width: 280px;\n --fluent-card-shadow: none;\n}"; + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/animation.ts +let animation_e = L.form; + + +const registerAnimationTab = (i)=>{ + i.tab("animation", _("Animation")); + { + let o = i.taboption("animation", animation_e.Flag, "view_transition", _("Enable page transition animation"), _("Use the browser View Transition API to animate navigation between LuCI pages when supported.")); + o.default = fluentFlagDefault(FLUENT_DEFAULTS.view_transition) ? o.enabled : o.disabled, omitDefaultValue(o); + } + { + let o = i.taboption("animation", animation_e.Flag, "tab_animation", _("Enable tab underline animation"), _("Animate the active underline when switching between native LuCI tabs and themed tab menus.")); + o.default = fluentFlagDefault(FLUENT_DEFAULTS.tab_animation) ? o.enabled : o.disabled, omitDefaultValue(o); + } + { + let o = i.taboption("animation", animation_e.Flag, "prefers_reduced_motion", _("Respect reduced-motion preference"), _("When enabled, Fluent animations follow the browser or operating system reduced-motion preference.")); + o.default = fluentFlagDefault(FLUENT_DEFAULTS.prefers_reduced_motion) ? o.enabled : o.disabled, omitDefaultValue(o); + } + { + let o = i.taboption("animation", animation_e.Flag, "loading_bar", _("Show top loading bar"), _("Display the themed loading indicator at the top edge during page loads and transitions.")); + o.default = fluentFlagDefault(FLUENT_DEFAULTS.loading_bar) ? o.enabled : o.disabled, omitDefaultValue(o); + } + { + let o = i.taboption("animation", animation_e.Flag, "uci_changes_preview", _("Show UCI changes while applying configuration"), _("Show pending UCI changes in the top apply-status message until the configuration apply completes.")); + o.default = fluentFlagDefault(FLUENT_DEFAULTS.uci_changes_preview) ? o.enabled : o.disabled, omitDefaultValue(o); + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/colors.ts +let colors_r = L.form; + + +const registerColorsTab = (d)=>{ + d.tab("colors", _("Colors")); + let l = createModeSubtabs(d, "colors", "colors_mode_tabs"); + { + let o = l.taboption("light", colors_r.Value, "primary", _("Accent color"), _("HEX color used as the primary Fluent accent when the interface is rendered in light mode.")); + o.default = FLUENT_DEFAULTS.primary, omitDefaultValue(o), configureHexColorValue(o, "primary"); + } + { + let o = l.taboption("light", colors_r.Value, "progressbar_font", _("Progress bar text color"), _("HEX color used for progress-bar labels while the interface is rendered in light mode.")); + o.default = FLUENT_DEFAULTS.progressbar_font, omitDefaultValue(o), configureHexColorValue(o, "progressbar_font"); + } + { + let o = l.taboption("light", colors_r.Value, "page_bg", _("Page background"), _("HEX color used for the main page background in light mode.")); + o.default = FLUENT_DEFAULTS.page_bg, omitDefaultValue(o), configureHexColorValue(o, "page_bg"); + } + { + let o = l.taboption("light", colors_r.Value, "card_bg", _("Card background"), _("HEX color used for container/card elements in light mode.")); + o.default = FLUENT_DEFAULTS.card_bg, omitDefaultValue(o), configureHexColorValue(o, "card_bg"); + } + { + let o = l.taboption("light", colors_r.Value, "sidebar_bg", _("Sidebar background"), _("HEX color used for the navigation sidebar in light mode.")); + o.default = FLUENT_DEFAULTS.sidebar_bg, omitDefaultValue(o), configureHexColorValue(o, "sidebar_bg"); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_primary", _("Accent color"), _("HEX color used as the primary Fluent accent when the interface is rendered in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_primary, omitDefaultValue(o), configureHexColorValue(o, "dark_primary", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_progressbar_font", _("Progress bar text color"), _("HEX color used for progress-bar labels while the interface is rendered in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_progressbar_font, omitDefaultValue(o), configureHexColorValue(o, "dark_progressbar_font", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_page_bg", _("Page background"), _("HEX color used for the main page background in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_page_bg, omitDefaultValue(o), configureHexColorValue(o, "dark_page_bg", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_card_bg", _("Card background"), _("HEX color used for container/card elements in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_card_bg, omitDefaultValue(o), configureHexColorValue(o, "dark_card_bg", !0); + } + { + let o = l.taboption("dark", colors_r.Value, "dark_sidebar_bg", _("Sidebar background"), _("HEX color used for the navigation sidebar in dark mode.")); + o.default = FLUENT_DEFAULTS.dark_sidebar_bg, omitDefaultValue(o), configureHexColorValue(o, "dark_sidebar_bg", !0); + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/general.ts +let general_e = L.form; + + +const registerGeneralTab = (l, r = !0)=>{ + l.tab("general", _("General")); + { + let o = l.taboption("general", general_e.ListValue, "mode", _("Color mode")); + o.value("auto", _("Follow system")), o.value("light", _("Force light mode")), o.value("dark", _("Force dark mode")), o.default = FLUENT_DEFAULTS.mode, omitDefaultValue(o), o.description = _("Use the system/browser preference, or always render the Fluent theme in a fixed light or dark palette."); + } + { + let o = l.taboption("general", general_e.ListValue, "direction_mode", _("Text direction")); + o.value("auto", _("Automatic (Arabic/Persian locales only)")), o.value("rtl", _("Force RTL")), o.value("ltr", _("Force LTR")), o.default = FLUENT_DEFAULTS.direction_mode, omitDefaultValue(o), o.description = _("Choose the document direction for authenticated and login pages. Automatic mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, fa-*) to RTL and falls back to LTR for missing or unrecognized locales."); + } + { + let o = l.taboption("general", general_e.ListValue, "font_weight", _("Navigation font weight")); + o.value("normal", _("Normal")), o.value("600", _("Semibold")), o.default = FLUENT_DEFAULTS.font_weight, omitDefaultValue(o), o.description = _("Controls the font weight used by main navigation labels and related theme text accents."); + } + { + let o = l.taboption("general", general_e.ListValue, "control_height", _("Control height")); + o.value("32", _("Compact (32px)")), o.value("42", _("Comfortable (42px)")), o.default = FLUENT_DEFAULTS.control_height, omitDefaultValue(o), o.description = _("Applies to standard buttons, inputs, selects, and similar form controls across the theme."); + } + if (r) { + let r = l.taboption("general", general_e.Flag, "custom_select", _("Use Fluent custom select dropdowns"), _("Replace native select elements with the theme's custom dropdown widget.")); + r.default = fluentFlagDefault(FLUENT_DEFAULTS.custom_select) ? r.enabled : r.disabled, omitDefaultValue(r); + } + { + let r = l.taboption("general", general_e.Flag, "sticky_action_bar", _("Keep action bar visible while scrolling"), _("Keep Save, Apply, and Reset actions fixed at the bottom of the viewport while scrolling.")); + r.default = fluentFlagDefault(FLUENT_DEFAULTS.sticky_action_bar) ? r.enabled : r.disabled, omitDefaultValue(r); + } + { + let o = l.taboption("general", general_e.ListValue, "progressbar_text_position", _("Progress bar text position")); + o.value("top-start", _("Above bar, start")), o.value("bottom-start", _("Below bar, start")), o.value("top-end", _("Above bar, end")), o.value("bottom-end", _("Below bar, end")), o.default = FLUENT_DEFAULTS.progressbar_text_position, omitDefaultValue(o), o.description = _("Position of progress-bar labels relative to the bar. Start/end alignment follows the text direction (LTR or RTL)."); + } +}; + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/login.tsx + +let login_a = L.form, login_n = L.rpc, login_l = L.fs, login_i = L.ui, login_o = login_n.declare({ + object: "luci.fluent", + method: "avail", + expect: { + avail: 0 + } +}), login_r = login_n.declare({ + object: "luci.fluent", + method: "remove", + params: [ + "filename" + ], + expect: { + result: 0 + } +}), login_d = login_n.declare({ + object: "luci.fluent", + method: "rename", + params: [ + "newname" + ], + expect: { + result: 0 + } +}); + + +let login_p = new Set([ + "jpg", + "jpeg", + "png", + "gif", + "webp", + "mp4", + "webm" +]), login_b = (e)=>e.split(".").pop()?.toLowerCase() ?? "", login_m = (e)=>login_p.has(login_b(e)), login_f = (e)=>e >= 1048576 ? `${(e / 1024 / 1024).toFixed(1)} GiB` : e >= 1024 ? `${(e / 1024).toFixed(1)} MiB` : `${e} KiB`, login_h = (e, t)=>{ + if (0 !== e) throw Error(`${t} failed with code ${e}.`); +}, login_v = (e, t)=>{ + dom.content(e, t); +}, login_k = login_a.DummyValue.extend({ + renderWidget: (a, n, c)=>{ + let s = jsx("div", { + class: "cbi-value-description fluent-bg-status", + children: "Ready to upload or remove custom backgrounds." + }), u = jsx("div", { + class: "fluent-bg-hint", + children: "Supported formats: JPG, PNG, GIF, WEBP, MP4, WEBM." + }), g = jsx("button", { + class: "btn cbi-button cbi-button-action", + type: "button", + children: "Upload background" + }), p = jsx("div", { + class: "fluent-bg-actions", + children: g + }), k = jsx("div", { + class: "fluent-bg-list" + }), y = (e)=>{ + login_v(s, [ + document.createTextNode(e) + ]); + }, $ = ()=>login_l.list("/www/luci-static/fluent/background").catch(()=>[]).then((a)=>{ + let n = a.filter((e)=>"file" === e.type && login_m(String(e.name ?? ""))).sort((e, t)=>String(e.name ?? "").localeCompare(String(t.name ?? ""))); + n.length ? login_v(k, n.map((a)=>{ + let n, l, o, d = String(a.name ?? ""), c = jsx("button", { + class: "btn cbi-button cbi-button-remove", + type: "button", + children: "Delete" + }), s = login_i.createHandlerFn(undefined, ()=>(y(`Deleting ${d}...`), login_r(d).then((e)=>(login_h(e, `Deleting ${d}`), $().then(()=>{ + y(`Deleted ${d}.`); + }))).catch((e)=>{ + y(`Failed to delete ${d}: ${e instanceof Error ? e.message : String(e)}`); + }))); + return s && c.addEventListener("click", s), jsxs("div", { + class: "fluent-bg-item", + children: [ + (n = login_b(d), l = `/luci-static/fluent/background/${encodeURIComponent(d)}`, "mp4" === n || "webm" === n ? jsx("video", { + class: "fluent-bg-preview fluent-bg-preview-video", + muted: !0, + playsInline: !0, + preload: "metadata", + src: l + }) : jsx("div", { + class: "fluent-bg-preview", + style: `background-image:url('${l.replace(/'/g, "%27")}')` + })), + jsxs("div", { + class: "fluent-bg-meta", + children: [ + jsx("strong", { + class: "fluent-bg-name", + title: d, + children: d + }), + jsx("span", { + class: "fluent-bg-size", + children: (o = Number(a.size ?? 0)) > 0 ? login_f(Math.max(1, Math.ceil(o / 1024))) : "Unknown size" + }) + ] + }), + jsx("div", { + class: "fluent-bg-item-actions", + children: c + }) + ] + }); + })) : login_v(k, jsxs("div", { + class: "fluent-bg-empty", + children: [ + jsx("strong", { + children: "No custom backgrounds yet" + }), + jsx("span", { + children: "Upload an image or video to use it on the login page." + }) + ] + })); + }); + return g.addEventListener("click", ()=>{ + y("Selecting background file..."), login_i.uploadFile("/tmp/fluent_background.tmp").then((e)=>{ + let t; + if (!e?.name) throw Error("Upload did not return a filename."); + let a = "" !== (t = e.name.replace(/^.*[\\/]/, "").replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "")) && "." !== t && ".." !== t && login_m(t) ? t : `background-${Date.now()}.jpg`; + return y(`Saving ${a}...`), login_d(a).then((e)=>(login_h(e, `Saving ${a}`), $().then(()=>{ + y(`Saved ${a}.`); + }))).catch((e)=>{ + y(`Failed to save ${a}: ${e instanceof Error ? e.message : String(e)}`); + }); + }).catch((e)=>{ + let t = e instanceof Error ? e.message : String(e); + t && "false" !== t ? y(`Upload failed: ${t}`) : y("Upload canceled."); + }); + }), login_o().then((e)=>{ + y(`Ready to upload or remove custom backgrounds. Available space: ${login_f(e)}.`); + }).catch(()=>{ + y("Ready to upload or remove custom backgrounds."); + }), $(), jsx("div", { + class: "fluent-bg-manager", + children: [ + s, + u, + p, + k + ] + }); + } +}); +const registerLoginTab = (e)=>{ + e.tab("login", _("Login page"), _("Customize the login page background, card opacity, and blur radius for light and dark modes.")); + { + let t = e.taboption("login", login_a.ListValue, "login_bg", "Background source", "Select the background image source for the login page."); + t.value("microsoft", "Microsoft dynamic canvas"), t.value("custom", "Custom background"), t.value("bing", "Bing daily wallpaper"), t.default = FLUENT_DEFAULTS.login_bg, omitDefaultValue(t); + } + e.taboption("login", login_k, "_bg_mgr", "Custom backgrounds", "Upload and manage custom background images for the login page.").depends("login_bg", "custom"); + let t = createModeSubtabs(e, "login", "login_mode_tabs"); + { + let e = t.taboption("light", login_a.ListValue, "transparency", _("Login card opacity"), _("Opacity of the login card in light mode. 0 is fully transparent and 1 is fully opaque.")); + for (let t of transparencySteps)e.value(String(t)); + e.default = FLUENT_DEFAULTS.transparency, omitDefaultValue(e); + } + { + let e = t.taboption("light", login_a.Value, "blur", _("Backdrop blur radius"), _("Blur radius in pixels behind the login card in light mode. Use 0 to disable blur.")); + e.datatype = "ufloat", e.default = FLUENT_DEFAULTS.blur, omitDefaultValue(e); + } + { + let e = t.taboption("dark", login_a.ListValue, "transparency_dark", _("Login card opacity"), _("Opacity of the login card in dark mode. 0 is fully transparent and 1 is fully opaque.")); + for (let t of transparencySteps)e.value(String(t)); + e.default = FLUENT_DEFAULTS.transparency_dark, omitDefaultValue(e); + } + { + let e = t.taboption("dark", login_a.Value, "blur_dark", _("Backdrop blur radius"), _("Blur radius in pixels behind the login card in dark mode. Use 0 to disable blur.")); + e.datatype = "ufloat", e.default = FLUENT_DEFAULTS.blur_dark, omitDefaultValue(e); + } +}; + +;// CONCATENATED MODULE: ./web/resources/menu-layout.ts +let menu_layout_e = "primary:", menu_layout_t = new Set([ + "version", + "custom", + "titles", + "itemTitles", + "categoryMoves", + "itemMoves", + "hiddenCategories", + "hiddenItems" +]); +function menu_layout_n(e) { + return "function" == typeof _ ? _(e) : e; +} +function primaryCategoryId(t) { + return `${menu_layout_e}${t}`; +} +function menu_layout_i(t) { + return t.startsWith(menu_layout_e) ? t.slice(menu_layout_e.length) : null; +} +function menu_layout_r(e) { + let t = [], i = []; + for (let r of ui.menu.getChildren(e))if (r.satisfied) for (let e of ui.menu.getChildren(r)){ + let l = e.title?.trim(); + if (!e.satisfied || !l) continue; + let o = `${r.name}/${e.name}`, u = menu_layout_n(l); + for (let a of (t.push({ + path: o, + pathSegments: [ + r.name, + e.name + ], + node: e, + title: u, + rawTitle: l + }), ui.menu.getChildren(e))){ + let t = a.title?.trim(); + a.satisfied && t && i.push({ + path: `${o}/${a.name}`, + pathSegments: [ + r.name, + e.name, + a.name + ], + node: a, + title: menu_layout_n(t), + rawTitle: t, + originalPrimaryPath: o, + originalPrimaryTitle: u + }); + } + } + return { + primaries: t, + items: i + }; +} +function discoverMenuItems(e) { + return menu_layout_r(e).items; +} +function menu_layout_l(e) { + let t = e.primaries.map((e)=>({ + id: primaryCategoryId(e.path), + title: e.title, + items: [] + })), n = new Map(t.map((e)=>[ + e.id, + e + ])); + for (let t of e.items)n.get(primaryCategoryId(t.originalPrimaryPath))?.items.push(t.path); + return t; +} +function buildDefaultMenuCategories(e) { + return menu_layout_l(menu_layout_r(e)); +} +function menu_layout_o(e) { + return e.map((e)=>({ + ...e, + items: [ + ...e.items + ] + })); +} +function moveMenuItem(e, t, n, i) { + let r = menu_layout_o(e), l = r.find((e)=>e.id === n); + if (!l || i === t) return r; + for (let e of r)e.items = e.items.filter((e)=>e !== t); + let u = i ? l.items.indexOf(i) : -1; + return u >= 0 ? l.items.splice(u, 0, t) : l.items.push(t), r; +} +function moveMenuCategory(e, t, n, i = "before") { + let r = menu_layout_o(e), l = r.findIndex((e)=>e.id === t); + if (l < 0 || t === n || !r.some((e)=>e.id === n)) return r; + let [u] = r.splice(l, 1), a = r.findIndex((e)=>e.id === n); + return r.splice("after" === i ? a + 1 : a, 0, u), r; +} +function menu_layout_u(e, t) { + let n = e[t]; + return new Set(e.slice(t + 1).filter((e)=>e.originalPrimaryPath === n.originalPrimaryPath).map((e)=>e.path)); +} +function canRestoreMenuItem(e, t, n) { + let i = t.findIndex((e)=>e.path === n); + if (i < 0) return !1; + let r = primaryCategoryId(t[i].originalPrimaryPath), l = e.find((e)=>e.id === r); + if (!l) return !0; + let o = 0, a = -1; + for (let t of e)for(let e = 0; e < t.items.length; e += 1)t.items[e] === n && (o += 1, t.id === r && (a = e)); + if (1 !== o || a < 0) return !0; + let s = menu_layout_u(t, i), f = l.items.findIndex((e)=>s.has(e)); + return (f < 0 ? l.items.length - 1 : f > a ? f - 1 : f) !== a; +} +function restoreMenuItemToOriginalPosition(e, t, n) { + let i = t.findIndex((e)=>e.path === n); + if (i < 0) return menu_layout_o(e); + let r = t[i], l = primaryCategoryId(r.originalPrimaryPath), a = e.some((e)=>e.id === l) ? e : [ + ...e, + { + id: l, + title: r.originalPrimaryTitle, + items: [] + } + ], s = menu_layout_u(t, i), f = a.find((e)=>e.id === l); + return moveMenuItem(a, n, l, f?.items.find((e)=>s.has(e))); +} +function menu_layout_a(e, t) { + if ("string" != typeof e || e !== e.trim()) return !1; + let n = e.split("/"); + return n.length === t && n.every(Boolean); +} +function menu_layout_s(e) { + return "number" == typeof e ? `custom:${e}` : `primary:${e}`; +} +function menu_layout_f(e, t) { + return Number.isInteger(e) && e >= 0 && e < t || menu_layout_a(e, 2) ? e : null; +} +function menu_layout_d(e, t) { + let n = e[t]; + return void 0 === n ? [] : Array.isArray(n) ? n : null; +} +function parseMenuLayout(e) { + let n; + if ("string" != typeof e || "" === e.trim()) return null; + try { + n = JSON.parse(e); + } catch { + return null; + } + if (!n || "object" != typeof n || Array.isArray(n)) return null; + let i = n; + if (1 !== i.version || Object.keys(i).some((e)=>!menu_layout_t.has(e))) return null; + let r = menu_layout_d(i, "custom"), l = menu_layout_d(i, "titles"), o = menu_layout_d(i, "itemTitles"), u = menu_layout_d(i, "categoryMoves"), m = menu_layout_d(i, "itemMoves"), p = menu_layout_d(i, "hiddenCategories"), h = menu_layout_d(i, "hiddenItems"); + if (!r || !l || !o || !u || !m || !p || !h) return null; + let g = [], y = new Set(); + for (let e of r){ + if ("string" != typeof e) return null; + let t = e.trim(), n = t.toLocaleLowerCase(); + if (!t || y.has(n)) return null; + y.add(n), g.push(t); + } + let c = [], M = new Set(); + for (let e of l){ + if (!Array.isArray(e) || 2 !== e.length || !menu_layout_a(e[0], 2) || "string" != typeof e[1]) return null; + let t = e[1].trim(); + if (!t || M.has(e[0])) return null; + M.add(e[0]), c.push([ + e[0], + t + ]); + } + let v = [], w = new Set(); + for (let e of o){ + if (!Array.isArray(e) || 2 !== e.length || !menu_layout_a(e[0], 3) || "string" != typeof e[1]) return null; + let t = e[1].trim(); + if (!t || w.has(e[0])) return null; + w.add(e[0]), v.push([ + e[0], + t + ]); + } + let I = [], C = new Set(); + for (let e of u){ + if (!Array.isArray(e) || 2 !== e.length) return null; + let t = menu_layout_f(e[0], g.length), n = null === e[1] ? null : menu_layout_f(e[1], g.length); + if (null === t || null !== e[1] && null === n) return null; + let i = menu_layout_s(t); + if (C.has(i) || null !== n && i === menu_layout_s(n)) return null; + C.add(i), I.push([ + t, + n + ]); + } + let S = [], x = new Set(); + for (let e of m){ + if (!Array.isArray(e) || 3 !== e.length || !menu_layout_a(e[0], 3)) return null; + let t = menu_layout_f(e[1], g.length), n = null === e[2] ? null : menu_layout_a(e[2], 3) ? e[2] : null; + if (null === t || null !== e[2] && null === n || x.has(e[0]) || n === e[0]) return null; + x.add(e[0]), S.push([ + e[0], + t, + n + ]); + } + let P = [], A = new Set(); + for (let e of p){ + let t = menu_layout_f(e, g.length); + if (null === t || A.has(menu_layout_s(t))) return null; + A.add(menu_layout_s(t)), P.push(t); + } + let T = [], b = new Set(); + for (let e of h){ + if (!menu_layout_a(e, 3) || b.has(e)) return null; + b.add(e), T.push(e); + } + return { + version: 1, + custom: g, + titles: c, + itemTitles: v, + categoryMoves: I, + itemMoves: S, + hiddenCategories: P, + hiddenItems: T + }; +} +function menu_layout_m() { + return { + titles: [], + itemTitles: [], + categoryMoves: [], + itemMoves: [] + }; +} +function menu_layout_p(e, t) { + return "number" == typeof e ? t[e] ?? null : primaryCategoryId(e); +} +function resolveMenuLayout(e, t) { + let n = menu_layout_r(e), i = menu_layout_l(n), u = new Set(), a = new Set(), s = menu_layout_m(); + if (!t) return { + categories: i, + hiddenCategoryIds: u, + hiddenItemPaths: a, + itemTitles: new Map(), + pending: s, + configured: !1 + }; + let f = t.custom.map((e, t)=>{ + let n = `custom:${t}`; + return i.push({ + id: n, + title: e, + items: [] + }), n; + }), d = new Set(n.items.map((e)=>e.path)), h = f.length > 0; + for (let [e, n] of t.titles){ + let t = primaryCategoryId(e), r = i.find((e)=>e.id === t); + r ? (r.title = n, h = !0) : s.titles.push([ + t, + n + ]); + } + let g = new Map(); + for (let [e, n] of t.itemTitles)d.has(e) ? (g.set(e, n), h = !0) : s.itemTitles.push([ + e, + n + ]); + for (let [e, n] of t.categoryMoves){ + let t = menu_layout_p(e, f), r = null === n ? null : menu_layout_p(n, f), l = null !== t && i.some((e)=>e.id === t), u = null === r || i.some((e)=>e.id === r); + t && l && u ? (i = function(e, t, n) { + if (n) return moveMenuCategory(e, t, n); + let i = menu_layout_o(e), r = i.findIndex((e)=>e.id === t); + if (r < 0) return i; + let [l] = i.splice(r, 1); + return i.push(l), i; + }(i, t, r), h = !0) : t && s.categoryMoves.push([ + t, + r + ]); + } + for (let [e, n, r] of t.itemMoves){ + let t = menu_layout_p(n, f), l = t ? i.find((e)=>e.id === t) : void 0, o = null === r || !!l?.items.includes(r); + d.has(e) && t && l && o ? (i = moveMenuItem(i, e, t, r ?? void 0), h = !0) : t && s.itemMoves.push([ + e, + t, + r + ]); + } + for (let e of t.hiddenCategories){ + let t = menu_layout_p(e, f); + t && (u.add(t), i.some((e)=>e.id === t) && (h = !0)); + } + for (let e of t.hiddenItems)a.add(e), d.has(e) && (h = !0); + return { + categories: i, + hiddenCategoryIds: u, + hiddenItemPaths: a, + itemTitles: g, + pending: s, + configured: h + }; +} +function menu_layout_h(e, t) { + let n = new Map(t.map((e, t)=>[ + e, + t + ])), i = e.flatMap((e)=>{ + let t = n.get(e); + return void 0 === t ? [] : [ + { + value: e, + index: t + } + ]; + }), r = [], l = Array(i.length).fill(-1); + for(let e = 0; e < i.length; e += 1){ + let t = 0, n = r.length; + for(; t < n;){ + let l = t + n >> 1; + i[r[l]].index < i[e].index ? t = l + 1 : n = l; + } + t > 0 && (l[e] = r[t - 1]), r[t] = e; + } + let o = new Set(); + for(let e = r.at(-1) ?? -1; e >= 0; e = l[e])o.add(i[e].value); + return o; +} +function menu_layout_g(e, t) { + let n = menu_layout_i(e); + return n || (t.get(e) ?? null); +} +function menu_layout_y(e, t, n) { + let i = n.get(e), r = n.get(t); + return void 0 !== i && void 0 !== r ? i - r : void 0 !== i ? -1 : void 0 !== r ? 1 : e.localeCompare(t); +} +function serializeMenuLayout(e, t, n, o, u = menu_layout_m(), a = new Map()) { + let s = menu_layout_r(e), f = menu_layout_l(s), d = new Map(f.map((e)=>[ + e.id, + e + ])), p = t.filter((e)=>!d.has(e.id)), c = new Map(p.map((e, t)=>[ + e.id, + t + ])), M = p.map((e)=>e.title.trim()), v = [], w = new Set(); + for (let e of t){ + let t = d.get(e.id), n = e.title.trim(); + if (!t || n === t.title) continue; + let r = menu_layout_i(e.id); + r && (v.push([ + r, + n + ]), w.add(e.id)); + } + for (let [e, t] of u.titles){ + if (w.has(e)) continue; + let n = menu_layout_i(e); + n && v.push([ + n, + t + ]); + } + let I = new Map(s.items.map((e)=>[ + e.path, + e.title + ])), C = [], S = new Set(); + for (let [e, t] of a){ + let n = t.trim(); + n && n !== I.get(e) && (C.push([ + e, + n + ]), S.add(e)); + } + for (let [e, t] of u.itemTitles)S.has(e) || C.push([ + e, + t + ]); + let x = [ + ...f.map((e)=>e.id), + ...p.map((e)=>e.id) + ], P = t.map((e)=>e.id), A = [], T = new Set(); + for (let [e, t] of function(e, t) { + let n = menu_layout_h(e, t), i = []; + for(let e = t.length - 1; e >= 0; e -= 1)n.has(t[e]) || i.push([ + t[e], + t[e + 1] ?? null + ]); + return i; + }(x, P)){ + let n = menu_layout_g(e, c), i = null === t ? null : menu_layout_g(t, c); + null !== n && (null === t || null !== i) && (A.push([ + n, + i + ]), T.add(e)); + } + for (let [e, t] of u.categoryMoves){ + if (T.has(e)) continue; + let n = menu_layout_g(e, c), i = null === t ? null : menu_layout_g(t, c); + null !== n && (null === t || null !== i) && A.push([ + n, + i + ]); + } + let b = [], $ = new Set(); + for (let e of t){ + let t = menu_layout_g(e.id, c); + if (null === t) continue; + let n = menu_layout_h(d.get(e.id)?.items ?? [], e.items); + for(let i = e.items.length - 1; i >= 0; i -= 1){ + let r = e.items[i]; + n.has(r) || (b.push([ + r, + t, + e.items[i + 1] ?? null + ]), $.add(r)); + } + } + for (let [e, t, n] of u.itemMoves){ + if ($.has(e)) continue; + let i = menu_layout_g(t, c); + null !== i && b.push([ + e, + i, + n + ]); + } + let L = new Map(t.map((e, t)=>[ + e.id, + t + ])), O = [ + ...n + ].sort((e, t)=>menu_layout_y(e, t, L)).flatMap((e)=>{ + let t = menu_layout_g(e, c); + return null === t ? [] : [ + t + ]; + }), j = new Map(s.items.map((e, t)=>[ + e.path, + t + ])), N = [ + ...o + ].sort((e, t)=>menu_layout_y(e, t, j)), k = { + version: 1 + }; + return M.length > 0 && (k.custom = M), v.length > 0 && (k.titles = v), C.length > 0 && (k.itemTitles = C), A.length > 0 && (k.categoryMoves = A), b.length > 0 && (k.itemMoves = b), O.length > 0 && (k.hiddenCategories = O), N.length > 0 && (k.hiddenItems = N), 1 === Object.keys(k).length ? "" : JSON.stringify(k); +} +function buildMenuPresentation(e, t) { + let n = menu_layout_r(e), l = resolveMenuLayout(e, parseMenuLayout(t)), o = new Map(n.items.map((e)=>[ + e.path, + e + ])); + for (let [e, t] of l.itemTitles){ + let n = o.get(e); + n && o.set(e, { + ...n, + title: t + }); + } + let u = new Map(n.primaries.map((e)=>[ + primaryCategoryId(e.path), + e + ])), a = new Set(l.hiddenItemPaths); + for (let e of l.hiddenCategoryIds){ + let t = menu_layout_i(e); + t && a.add(t); + let n = l.categories.find((t)=>t.id === e); + if (n) for (let e of n.items)a.add(e); + } + return { + categories: l.categories.map((e)=>({ + id: e.id, + title: e.title, + primary: u.get(e.id) ?? null, + items: e.items.flatMap((e)=>{ + let t = o.get(e); + return t ? [ + t + ] : []; + }) + })), + hiddenCategoryIds: l.hiddenCategoryIds, + hiddenPaths: a, + configured: l.configured + }; +} + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config/tabs/menu.tsx + +let menu_n = L.form; + +function menu_g(e, t) { + let i = t.getBoundingClientRect(); + return e.clientY < i.top + i.height / 2 ? "before" : "after"; +} +let menu_f = 0; +function menu_h(e) { + return e.trim().toLocaleLowerCase(); +} +function menu_y(e) { + let t = new Map(), i = new Map(); + for (let n of e){ + let e = n.title.trim(), l = menu_h(e); + if (!e) { + t.set(n.id, _("Primary menu titles cannot be empty.")); + continue; + } + let r = i.get(l); + r ? (t.set(r, _("Primary menu titles must be unique.")), t.set(n.id, _("Primary menu titles must be unique."))) : i.set(l, n.id); + } + return t; +} +function registerMenuTab(b, v) { + var I; + let k, M, w, C; + b.tab("menu", _("Menu")); + let E = b.taboption("menu", (I = v, M = new Map((k = discoverMenuItems(I)).map((e)=>[ + e.path, + e + ])), w = new Map(buildDefaultMenuCategories(I).map((e)=>[ + e.id, + e + ])), C = new Map(), menu_n.Value.extend({ + renderWidget (n, o, b) { + let v = "string" == typeof b || Array.isArray(b) ? b : null, E = "string" == typeof b ? b : "", x = function(e, t) { + let i = parseMenuLayout(t); + if (!i) return { + categories: buildDefaultMenuCategories(e), + hiddenCategoryIds: new Set(), + hiddenItemPaths: new Set(), + itemTitles: new Map(), + pending: { + titles: [], + itemTitles: [], + categoryMoves: [], + itemMoves: [] + } + }; + let n = resolveMenuLayout(e, i); + return { + categories: n.categories, + hiddenCategoryIds: n.hiddenCategoryIds, + hiddenItemPaths: n.hiddenItemPaths, + itemTitles: new Map(n.itemTitles), + pending: n.pending + }; + }(I, v), P = new Set(), S = "" === E ? null : parseMenuLayout(E), T = "" !== E && !S, A = jsx("input", { + type: "hidden", + id: this.cbid(n), + value: E + }), R = jsx("div", { + class: "fluent-menu-editor__categories" + }), $ = new Map(), q = null, X = null, Y = null, D = null, B = null, F = jsx("div", { + class: "fluent-menu-editor__validation", + role: "alert" + }), N = jsx("div", { + class: "fluent-menu-editor__notice", + hidden: !T, + children: _('The stored menu layout is invalid. Choose "Restore defaults" or make an edit before saving.') + }); + C.set(n, A); + let U = ()=>{ + let e = menu_y(x.categories); + for (let [t, i] of $){ + let n = e.get(t) ?? ""; + i.card.classList.toggle("is-invalid", !!n), i.error && (i.error.textContent = n); + } + F.textContent = e.size > 0 ? _("Fix primary menu title errors before saving.") : ""; + }, V = (e = serializeMenuLayout(I, x.categories, x.hiddenCategoryIds, x.hiddenItemPaths, x.pending, x.itemTitles))=>{ + A.value = e, N.hidden = !0, U(), A.dispatchEvent(new Event("change", { + bubbles: !0 + })); + }, j = ()=>"true" === document.body.dataset.reduceMotion, z = ()=>{ + q?.classList.remove("is-drop-before", "is-drop-after"), q = null, X?.classList.remove("is-drag-over"), X = null; + }, W = (e, t)=>{ + let i, n, l, r, o = D; + if (!o || j() || o.placeholder.parentElement === e && o.placeholder.nextSibling === t) return; + let a = (i = "category" === o.source.kind ? ".fluent-menu-editor__category" : ".fluent-menu-editor__item", Array.from(R.querySelectorAll(i)).filter((e)=>e !== D?.sourceElement && e.getClientRects().length > 0)), d = new Map(a.map((e)=>[ + e, + e.getBoundingClientRect() + ])); + e.insertBefore(o.placeholder, t); + let s = { + duration: Number.isFinite(r = Number.parseFloat(l = (n = getComputedStyle(document.documentElement)).getPropertyValue("--fluent-duration-fast").trim())) ? r * (l.endsWith("ms") ? 1 : 1000) : 0, + easing: n.getPropertyValue("--fluent-easing-standard").trim() + }; + for (let e of a){ + let t = d.get(e); + if (!t || !e.isConnected) continue; + let i = e.getBoundingClientRect(), n = t.left - i.left, l = t.top - i.top; + (0 !== n || 0 !== l) && (e.getAnimations().forEach((e)=>{ + e.cancel(); + }), e.animate([ + { + transform: `translate(${n}px, ${l}px)` + }, + { + transform: "translate(0, 0)" + } + ], s)); + } + }, O = (e = !0)=>{ + let t, i; + z(), Y = null, e && (t = D, i = t?.sourceElement.parentElement, t && i && W(i, t.sourceElement)); + }, G = (e)=>{ + var t, i, n; + if (!(t = Y) || t.kind !== e.kind || t.categoryId !== e.categoryId || ("item" === t.kind && "item" === e.kind ? t.path !== e.path || t.position !== e.position : "category" === t.kind && "category" === e.kind ? t.categoryId !== e.categoryId || t.position !== e.position : "item-list" !== t.kind && ("item-category" !== t.kind || "item-category" !== e.kind || t.position !== e.position))) { + if (z(), Y = e, j()) return void ("item-list" === e.kind ? (X = e.element, e.element.classList.add("is-drag-over")) : (i = e.element, n = e.position, z(), q = i, i.classList.toggle("is-drop-before", "before" === n), i.classList.toggle("is-drop-after", "after" === n))); + "item-category" === e.kind && (X = e.element, e.element.classList.add("is-drag-over")), ((e)=>{ + if (!D) return; + if ("category" === e.kind || "item" === e.kind) { + let t = "before" === e.position ? e.element : e.element.nextSibling, i = e.element.parentElement; + i && W(i, t); + return; + } + let t = "item-list" === e.kind ? e.element : e.element.querySelector(".fluent-menu-editor__items"); + t && W(t, null); + })(e); + } + }, H = (e, t, i)=>{ + M.has(e) && x.categories.some((e)=>e.id === t) && (x.pending.itemMoves = x.pending.itemMoves.filter(([t])=>t !== e), x.categories = moveMenuItem(x.categories, e, t, i), P.add(t), V(), eo()); + }, J = ()=>{ + let e = B; + if (e) { + let t; + B = null, e.sourceElement.classList.remove("is-dragging"), e.preview.remove(), O(), t = D, D = null, t && (t.placeholder.remove(), t.sourceElement.classList.remove("is-drag-source")), e.handle.hasPointerCapture(e.pointerId) && e.handle.releasePointerCapture(e.pointerId); + } + }, K = (e, t, i)=>{ + let n = Array.from(R.querySelectorAll(".fluent-menu-editor__category")).filter((e)=>{ + let t = e.dataset.categoryId; + return t && t !== i && !e.classList.contains("is-dragging"); + }), l = null, r = Number.MAX_VALUE; + for (let i of n){ + let n = i.getBoundingClientRect(), o = Math.hypot(e - (n.left + n.width / 2), t - (n.top + n.height / 2)); + o < r && (r = o, l = i); + } + return l; + }, Q = (e, t)=>{ + if (((e, t)=>{ + let i = D?.placeholder; + if (!i?.isConnected) return !1; + let n = i.getBoundingClientRect(); + return e >= n.left && e <= n.right && t >= n.top && t <= n.bottom; + })(t.clientX, t.clientY)) return; + let i = document.elementFromPoint(t.clientX, t.clientY); + if (!i) return void O(); + if ("category" === e.kind) { + let n = i.closest(".fluent-menu-editor__category"); + n || (n = K(t.clientX, t.clientY, e.categoryId)); + let l = n?.dataset.categoryId; + return n && l && l !== e.categoryId ? void G({ + kind: "category", + categoryId: l, + element: n, + position: menu_g(t, n) + }) : void O(); + } + let n = i.closest(".fluent-menu-editor__item"), l = n?.dataset.itemPath, r = n?.closest(".fluent-menu-editor__items"), o = r?.dataset.categoryId; + if (n && l && r && o) return l === e.path ? void O() : void G({ + kind: "item", + categoryId: o, + element: n, + path: l, + position: menu_g(t, n) + }); + let a = i.closest(".fluent-menu-editor__items"); + if (a) { + let e = ((e, t, i)=>{ + let n = e.dataset.categoryId; + if (!n) return null; + let l = Array.from(e.querySelectorAll(":scope > .fluent-menu-editor__item")).filter((e)=>e !== D?.sourceElement && !e.classList.contains("is-dragging") && e.getClientRects().length > 0).map((e)=>({ + element: e, + path: e.dataset.itemPath, + bounds: e.getBoundingClientRect() + })).filter((e)=>!!e.path); + if (0 === l.length) return { + kind: "item-list", + categoryId: n, + element: e + }; + let r = []; + for (let e of l){ + let t = r.at(-1); + !t || e.bounds.top >= t.bottom - 1 ? r.push({ + items: [ + e + ], + top: e.bounds.top, + bottom: e.bounds.bottom + }) : (t.items.push(e), t.top = Math.min(t.top, e.bounds.top), t.bottom = Math.max(t.bottom, e.bounds.bottom)); + } + let o = (e, t)=>({ + kind: "item", + categoryId: n, + element: e.element, + path: e.path, + position: t + }), a = l[0]; + if (i < r[0].top) return o(a, "before"); + for(let n = 0; n < r.length; n += 1){ + let l = r[n]; + if (i <= l.bottom) { + if (1 === l.items.length) { + let e = l.items[0]; + return o(e, i < e.bounds.top + e.bounds.height / 2 ? "before" : "after"); + } + let n = l.items.reduce((e, i)=>Math.abs(t - i.bounds.left - i.bounds.width / 2) < Math.abs(t - e.bounds.left - e.bounds.width / 2) ? i : e), r = n.bounds.left + n.bounds.width / 2; + return o(n, ("rtl" === getComputedStyle(e).direction ? t > r : t < r) ? "before" : "after"); + } + let a = r[n + 1]; + if (a && i < a.top) return o(a.items[0], "before"); + } + return { + kind: "item-list", + categoryId: n, + element: e + }; + })(a, t.clientX, t.clientY); + e ? G(e) : O(); + return; + } + let d = i.closest(".fluent-menu-editor__category"); + d || (d = K(t.clientX, t.clientY)); + let s = d?.dataset.categoryId; + d && s ? G({ + kind: "item-category", + categoryId: s, + element: d, + position: "after" + }) : O(); + }, Z = (e)=>{ + let t = B; + t && t.pointerId === e.pointerId && (e.preventDefault(), t.preview.style.left = `${e.clientX - t.offsetX}px`, t.preview.style.top = `${e.clientY - t.offsetY}px`, Q(t.source, e)); + }, ee = (e, t)=>{ + let i = B; + if (i && i.pointerId === e.pointerId) if (e.preventDefault(), t) { + let t, n; + Q(i.source, e), t = B, n = Y, J(), t && n && ((e, t)=>{ + if ("item" === e.kind) { + if ("item" === t.kind) { + let i = x.categories.find((e)=>e.id === t.categoryId); + if (!i) return; + let n = i.items.indexOf(t.path); + if (n < 0) return; + let l = "before" === t.position ? t.path : i.items[n + 1]; + H(e.path, t.categoryId, l); + } else ("item-category" === t.kind || "item-list" === t.kind) && H(e.path, t.categoryId); + return; + } + "category" === t.kind && (x.pending.categoryMoves = x.pending.categoryMoves.filter(([t])=>t !== e.categoryId), x.categories = moveMenuCategory(x.categories, e.categoryId, t.categoryId, t.position), V(), eo()); + })(t.source, n); + } else J(); + }, et = (e, t, i)=>{ + e.addEventListener("pointerdown", (n)=>((e, t, i, n)=>{ + if (B || "mouse" !== e.pointerType && "pen" !== e.pointerType && "touch" !== e.pointerType) return; + e.preventDefault(), e.stopPropagation(); + let l = i.getBoundingClientRect(), r = i.cloneNode(!0); + r.classList.add("fluent-menu-editor__drag-preview"), r.setAttribute("aria-hidden", "true"), "category" === t.kind && (r.querySelector(".fluent-menu-editor__category-error")?.remove(), r.querySelector(".fluent-menu-editor__items")?.remove()), r.style.width = `${l.width}px`, document.body.append(r); + let o = { + handle: n, + offsetX: e.clientX - l.left, + offsetY: e.clientY - l.top, + pointerId: e.pointerId, + preview: r, + source: t, + sourceElement: i + }; + B = o, i.classList.add("is-dragging"), ((e, t)=>{ + if (j()) return; + let i = t.getBoundingClientRect(), n = "category" === e.kind ? "category" : "item", l = document.createElement("div"); + l.className = `fluent-menu-editor__drop-placeholder fluent-menu-editor__drop-placeholder--${n}`, l.style.height = `${i.height}px`, l.setAttribute("aria-hidden", "true"), t.parentElement?.insertBefore(l, t), D = { + placeholder: l, + source: e, + sourceElement: t + }, t.classList.add("is-drag-source"); + })(t, i), r.style.left = `${e.clientX - o.offsetX}px`, r.style.top = `${e.clientY - o.offsetY}px`, n.setPointerCapture(e.pointerId); + })(n, t, i, e)), e.addEventListener("pointermove", Z), e.addEventListener("pointerup", (e)=>ee(e, !0)), e.addEventListener("pointercancel", (e)=>ee(e, !1)), e.addEventListener("lostpointercapture", ()=>{ + B?.handle === e && J(); + }); + }, ei = (i, l)=>{ + let o = M.get(i), a = x.categories.find((e)=>e.id === l); + if (!o || !a) return null; + let d = jsx("div", { + class: `fluent-menu-editor__item${x.hiddenItemPaths.has(i) ? " is-hidden" : ""}`, + "data-item-path": i + }), s = jsx("span", { + class: "fluent-menu-editor__drag-handle", + title: _("Drag to move second-level menu") + }), c = jsx("input", { + type: "checkbox", + checked: !x.hiddenItemPaths.has(i), + "aria-label": _("Show %s in the menu").format(o.title) + }); + et(s, { + kind: "item", + path: i + }, d), c.addEventListener("change", ()=>{ + c.checked ? x.hiddenItemPaths.delete(i) : x.hiddenItemPaths.add(i), d.classList.toggle("is-hidden", !c.checked), V(); + }); + let p = x.itemTitles.get(i) ?? o.title, g = _("Edit"), f = jsx("button", { + class: "fluent-menu-editor__category-edit", + type: "button", + "aria-label": g, + title: g + }); + f.addEventListener("click", ()=>{ + let l = x.itemTitles.get(i) ?? o.title, r = `${this.cbid(n)}-item-${i.replace(/\//g, "-")}-title`, a = jsx("input", { + id: r, + class: "cbi-input-text", + type: "text", + value: l, + "aria-label": _("Second-level menu title") + }), d = jsx("p", { + class: "cbi-value-description", + role: "alert", + hidden: !0 + }), s = null, u = ()=>{ + let e = a.value.trim() ? "" : _("Menu title cannot be empty."); + return d.textContent = e, d.hidden = !e, a.classList.toggle("cbi-input-invalid", !!e), s && (s.disabled = !!e), !e; + }; + a.addEventListener("input", u), s = jsx("button", { + type: "button", + class: "btn cbi-button-save", + onclick: ()=>{ + if (!u()) return; + let e = a.value.trim(); + e === o.title ? x.itemTitles.delete(i) : x.itemTitles.set(i, e), V(), eo(), L.ui.hideModal(); + }, + children: _("Save") + }); + let c = _("Restore to original title"), m = jsx("button", { + class: "fluent-menu-editor__item-reset", + type: "button", + "aria-label": c, + title: c, + onclick: ()=>{ + a.value = o.title, u(), a.focus(); + } + }); + L.ui.showModal(_("Second-level menu title"), jsxs("div", { + class: "fluent-menu-editor__rename-dialog", + children: [ + jsx("label", { + htmlFor: r, + children: _("Second-level menu title") + }), + jsxs("div", { + class: "fluent-menu-editor__rename-control", + children: [ + a, + m + ] + }), + d, + jsxs("div", { + class: "right", + children: [ + jsx("button", { + type: "button", + class: "btn", + onclick: ()=>L.ui.hideModal(), + children: _("Cancel") + }), + s + ] + }) + ] + })), u(), requestAnimationFrame(()=>{ + a.focus(), a.select(); + }); + }); + let h = jsxs("span", { + class: "fluent-menu-editor__item-label", + children: [ + jsx("strong", { + children: p + }), + jsx("small", { + children: o.path + }) + ] + }); + if (d.append(s, f, h), canRestoreMenuItem(x.categories, k, i)) { + let t = _("Restore %s to its original menu position").format(o.title), n = jsx("button", { + class: "fluent-menu-editor__item-reset", + type: "button", + "aria-label": t, + title: t + }); + n.addEventListener("click", ()=>{ + let e; + (e = M.get(i)) && P.add(primaryCategoryId(e.originalPrimaryPath)), x.pending.itemMoves = x.pending.itemMoves.filter(([e])=>e !== i), x.categories = restoreMenuItemToOriginalPosition(x.categories, k, i), V(), eo(); + }), d.append(n); + } + return d.append(c), d; + }, en = (l)=>{ + let r = document.createElement("details"); + r.className = `fluent-menu-editor__category${x.hiddenCategoryIds.has(l.id) ? " is-hidden" : ""}`, r.dataset.categoryId = l.id, r.open = P.has(l.id); + let o = document.createElement("summary"); + o.className = "fluent-menu-editor__category-header"; + let a = jsx("div", { + class: "fluent-menu-editor__items", + "data-category-id": l.id + }), d = jsx("span", { + class: "fluent-menu-editor__category-count", + title: _("%d second-level menus").format(l.items.length), + children: l.items.length + }), s = jsx("div", { + class: "fluent-menu-editor__category-error", + role: "alert" + }), u = !1, c = ()=>{ + if (u) return; + let t = l.items.flatMap((e)=>{ + let t = ei(e, l.id); + return t ? [ + t + ] : []; + }); + t.length > 0 ? a.append(...t) : a.appendChild(jsx("div", { + class: "fluent-menu-editor__empty", + children: _("Drop second-level menus here") + })), u = !0; + }; + r.addEventListener("toggle", ()=>{ + r.open ? (P.add(l.id), c()) : P.delete(l.id); + }), r.open && c(); + let p = x.categories.length > 1, g = w.get(l.id), f = jsx("input", { + type: "checkbox", + checked: !x.hiddenCategoryIds.has(l.id), + "aria-label": _("Show %s in the menu").format(l.title) + }), h = jsx("span", { + class: "fluent-menu-editor__category-name", + children: l.title + }), b = _("Edit"), v = jsx("button", { + class: "fluent-menu-editor__category-edit", + type: "button", + "aria-label": b, + title: b + }), I = ()=>{ + let r = `${this.cbid(n)}-${l.id}-title`, o = jsx("input", { + id: r, + class: "cbi-input-text", + type: "text", + value: l.title, + "aria-label": _("Primary menu title") + }), a = jsx("p", { + class: "cbi-value-description", + role: "alert", + hidden: !0 + }), d = null, s = ()=>{ + let e = o.value.trim(), t = menu_y(x.categories.map((t)=>t.id === l.id ? { + ...t, + title: e + } : t)).get(l.id) ?? ""; + return a.textContent = t, a.hidden = !t, o.classList.toggle("cbi-input-invalid", !!t), d && (d.disabled = !!t), !t; + }; + o.addEventListener("input", s), d = jsx("button", { + type: "button", + class: "btn cbi-button-save", + onclick: ()=>{ + s() && (l.title = o.value.trim(), V(), eo(), L.ui.hideModal()); + }, + children: _("Save") + }); + let u = g ? _("Restore %s to its original menu name").format(g.title) : "", c = g ? jsx("button", { + class: "fluent-menu-editor__item-reset", + type: "button", + "aria-label": u, + title: u, + onclick: ()=>{ + o.value = g.title, s(), o.focus(); + } + }) : null; + L.ui.showModal(_("Primary menu title"), jsxs(Fragment, { + children: [ + jsxs("div", { + class: "fluent-menu-editor__rename-dialog", + children: [ + jsx("label", { + htmlFor: r, + children: _("Primary menu title") + }), + jsxs("div", { + class: "fluent-menu-editor__rename-control", + children: [ + o, + c + ] + }), + a + ] + }), + jsxs("div", { + class: "right", + children: [ + jsx("button", { + type: "button", + class: "btn", + onclick: ()=>L.ui.hideModal(), + children: _("Cancel") + }), + d + ] + }) + ] + })), s(), requestAnimationFrame(()=>{ + o.focus(), o.select(); + }); + }; + if (v.addEventListener("click", I), f.addEventListener("change", ()=>{ + f.checked ? x.hiddenCategoryIds.delete(l.id) : x.hiddenCategoryIds.add(l.id), r.classList.toggle("is-hidden", !f.checked), V(); + }), p) { + let t = { + kind: "category", + categoryId: l.id + }, i = jsx("span", { + class: "fluent-menu-editor__drag-handle", + title: _("Drag to reorder primary menu") + }); + et(i, t, r), o.append(i); + } + if (o.append(v, h, d), !g) { + let t = jsx("button", { + class: "fluent-menu-editor__category-delete", + type: "button", + "aria-label": _("Delete primary menu"), + title: _("Delete primary menu") + }); + t.addEventListener("click", ()=>{ + let e = [ + ...l.items + ]; + for (let t of (x.categories = x.categories.filter((e)=>e.id !== l.id), x.hiddenCategoryIds.delete(l.id), x.pending.categoryMoves = x.pending.categoryMoves.filter(([e, t])=>e !== l.id && t !== l.id), x.pending.itemMoves = x.pending.itemMoves.filter(([, e])=>e !== l.id), P.delete(l.id), e))x.pending.itemMoves = x.pending.itemMoves.filter(([e])=>e !== t), x.categories = restoreMenuItemToOriginalPosition(x.categories, k, t); + V(), eo(); + }), o.append(t); + } + o.append(f), r.append(o), s && r.append(s), r.append(a); + let M = { + card: r + }; + return s && (M.error = s), $.set(l.id, M), r; + }, el = ()=>{ + let e = x.pending.titles.length + x.pending.itemTitles.length + x.pending.categoryMoves.length + x.pending.itemMoves.length; + for (let t of x.hiddenItemPaths)M.has(t) || (e += 1); + let t = new Set(x.categories.map((e)=>e.id)); + for (let i of x.hiddenCategoryIds)t.has(i) || (e += 1); + return e; + }, er = jsx("button", { + class: "btn cbi-button", + type: "button" + }); + function eo() { + let e, t = new Map(); + for (let e of R.querySelectorAll(".fluent-menu-editor__items")){ + let i = e.dataset.categoryId; + i && t.set(i, e.scrollTop); + } + let i = window.scrollX, n = window.scrollY; + $.clear(), dom.content(R, x.categories.map((e)=>en(e))), U(), er.disabled = 0 === (e = el()), e > 0 ? er.textContent = _("Clean up invalid items (%d)").format(e) : er.textContent = _("Clean up invalid items"), requestAnimationFrame(()=>{ + for (let e of R.querySelectorAll(".fluent-menu-editor__items")){ + let i = e.dataset.categoryId, n = i ? t.get(i) : void 0; + null != n && (e.scrollTop = n); + } + window.scrollTo(i, n); + }); + } + er.addEventListener("click", ()=>{ + let i = el(); + 0 !== i && L.ui.showModal(_("Clean up invalid items"), jsxs("div", { + class: "fluent-menu-editor__rename-dialog", + children: [ + jsx("p", { + children: _("Are you sure you want to clean up %d invalid/uninstalled menu items or rules?").format(i) + }), + jsxs("div", { + class: "right", + children: [ + jsx("button", { + type: "button", + class: "btn", + onclick: ()=>L.ui.hideModal(), + children: _("Cancel") + }), + jsx("button", { + type: "button", + class: "btn cbi-button-save", + onclick: ()=>{ + x.pending = { + titles: [], + itemTitles: [], + categoryMoves: [], + itemMoves: [] + }; + let e = new Set(); + for (let t of x.hiddenItemPaths)M.has(t) && e.add(t); + x.hiddenItemPaths = e; + let t = new Set(), i = new Set(x.categories.map((e)=>e.id)); + for (let e of x.hiddenCategoryIds)i.has(e) && t.add(e); + x.hiddenCategoryIds = t, V(), eo(), L.ui.hideModal(); + }, + children: _("Clean up") + }) + ] + }) + ] + })); + }); + let ea = jsx("button", { + class: "btn cbi-button cbi-button-add", + type: "button", + children: _("Add primary menu") + }), ed = jsx("button", { + class: "btn cbi-button cbi-button-reset", + type: "button", + children: _("Restore defaults") + }); + return ea.addEventListener("click", ()=>{ + let e = _("New primary menu"), t = new Set(x.categories.map((e)=>menu_h(e.title))), i = e, n = 2; + for(; t.has(menu_h(i));)i = _("New primary menu %d").format(n), n += 1; + let l = "u" > typeof crypto && "function" == typeof crypto.randomUUID ? crypto.randomUUID() : (menu_f += 1, `category-${Date.now()}-${menu_f}`); + x.categories.push({ + id: l, + title: i, + items: [] + }), P.add(l), V(), eo(); + }), ed.addEventListener("click", ()=>{ + x = { + categories: buildDefaultMenuCategories(I), + hiddenCategoryIds: new Set(), + hiddenItemPaths: new Set(), + itemTitles: new Map(), + pending: { + titles: [], + itemTitles: [], + categoryMoves: [], + itemMoves: [] + } + }, P.clear(), V(""), eo(); + }), eo(), jsxs("div", { + class: "fluent-menu-editor", + children: [ + A, + N, + jsx("div", { + class: "fluent-menu-editor__actions", + children: [ + er, + ea, + ed + ] + }), + F, + R + ] + }); + }, + formvalue: (e)=>C.get(e)?.value ?? "", + validate: (e, t)=>(function(e, t) { + if ("" === t || null == t) return !0; + if ("string" != typeof t) return _("The stored menu layout is invalid. Restore defaults or edit the layout before saving."); + let i = parseMenuLayout(t); + return i ? menu_y(resolveMenuLayout(e, i).categories).values().next().value ?? !0 : _("The stored menu layout is invalid. Restore defaults or edit the layout before saving."); + })(I, t) + })), "menu_layout"); + E.optional = !0, E.rmempty = !0; +} + +;// CONCATENATED MODULE: ./web/resources/view/fluent-config.tsx +let fluent_config_e = L.form, fluent_config_o = L.uci; + + + + + + + +class fluent_config_m extends L.view { + load() { + return Promise.all([ + fluent_config_o.load("fluent"), + L.ui.menu.load() + ]); + } + render(o) { + let [, m] = o, s = new fluent_config_e.Map("fluent", _("Fluent theme settings"), _("Configure color mode, accent colors, layout sizing, animation behavior, login-page appearance, and advanced CSS overrides for luci-theme-fluent.")), u = s.section(fluent_config_e.TypedSection, "global", _("Theme settings")); + return u.addremove = !1, u.anonymous = !0, registerGeneralTab(u), registerMenuTab(u, m), registerColorsTab(u), registerAnimationTab(u), registerLoginTab(u), registerAdvancedTab(u), registerAboutTab(u), s.render(); + } +} +const main = fluent_config_m; + + +return main; \ No newline at end of file diff --git a/luci-theme-fluent/po/de/fluent.po b/luci-theme-fluent/po/de/fluent.po new file mode 100644 index 000000000..26d23de86 --- /dev/null +++ b/luci-theme-fluent/po/de/fluent.po @@ -0,0 +1,778 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:37+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: de\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" + +msgid "%d second-level menus" +msgstr "%d Untermenüs" + +msgid "A new version is available!" +msgstr "Eine neue Version ist verfügbar!" + +msgid "About" +msgstr "Über" + +msgid "Above bar, end" +msgstr "Über dem Balken, Ende" + +msgid "Above bar, start" +msgstr "Über dem Balken, Anfang" + +msgid "Accent color" +msgstr "Akzentfarbe" + +msgid "Add primary menu" +msgstr "Hauptmenü hinzufügen" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Passen Sie Layout, Typografie, Übergangszeiten, Schatten an und fügen Sie " +"benutzerdefinierte CSS-Variablen oder -regeln hinzu, wenn die integrierten " +"Steuerelemente nicht ausreichen." + +msgid "Advanced" +msgstr "Erweitert" + +msgid "Advanced MAC Selector" +msgstr "Erweiterter MAC-Auswahler" + +msgid "and %d more..." +msgstr "und %d weitere..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Animieren Sie die aktive Unterlinie beim Wechsel zwischen nativen LuCI-Tabs " +"und thematischen Tab-Menüs." + +msgid "Animation" +msgstr "Animation" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"API-Ratenlimit überschritten. Bitte geben Sie ein GitHub-Token ein, um " +"fortzufahren. Dieses Token wird nur für Frontend-Anfragen verwendet und " +"nicht im Router-Backend gespeichert." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Gilt für Standard-Buttons, Eingabefelder, Auswahlmenüs und ähnliche " +"Formularsteuerelemente innerhalb des Themes." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Möchten Sie wirklich %d ungültige oder deinstallierte Menüpunkte oder Regeln " +"entfernen?" + +msgid "Author" +msgstr "Autor" + +msgid "Auto-scroll" +msgstr "Automatisches Scrollen" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Automatisch (nur für arabische/persische Regionen)" + +msgid "Backdrop blur radius" +msgstr "Radius der Hintergrundunschärfe" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Backend (GHProxy-Beschleunigung)" + +msgid "Backend (Official GitHub)" +msgstr "Backend (Offizielles GitHub)" + +msgid "Base font size" +msgstr "Grundschriftgröße" + +msgid "Below bar, end" +msgstr "Unterhalb des Balkens, Ende" + +msgid "Below bar, start" +msgstr "Unterhalb des Balkens, Anfang" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Unschärferadius in Pixeln hinter der Anmeldekarte im dunklen Modus. " +"Verwenden Sie 0, um die Unschärfe zu deaktivieren." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Unschärferadius in Pixeln hinter der Anmeldekarte im hellen Modus. Verwenden " +"Sie 0, um die Unschärfe zu deaktivieren." + +msgid "Cancel" +msgstr "Abbrechen" + +msgid "Card background" +msgstr "Kartenhintergrund" + +msgid "Card shadow" +msgstr "Kartenschatten" + +msgid "Check for updates" +msgstr "Nach Updates suchen" + +msgid "Checking for updates..." +msgstr "Suche nach Updates..." + +msgid "Choose color" +msgstr "Farbe auswählen" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Wählen Sie die Dokumentrichtung für authentifizierte Seiten und " +"Anmeldeseiten. Der automatische Modus löst arabische und persische " +"Ländercodes (ar, ar_*, ar-*, fa, fa_*, fa-*) in RTL auf und verwendet " +"standardmäßig LTR für fehlende oder nicht erkannte Ländercodes." + +msgid "Clean up" +msgstr "Bereinigen" + +msgid "Clean up invalid items" +msgstr "Ungültige Elemente bereinigen" + +msgid "Clean up invalid items (%d)" +msgstr "Ungültige Elemente bereinigen (%d)" + +msgid "Close" +msgstr "Schließen" + +msgid "Color mode" +msgstr "Farbmodus" + +msgid "Colors" +msgstr "Farben" + +msgid "Comfortable (42px)" +msgstr "Komfortabel (42px)" + +msgid "Compact (32px)" +msgstr "Kompakt (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Konfigurieren Sie den Farbmodus, Akzentfarben, Layout-Größen, " +"Animationsverhalten, das Erscheinungsbild der Anmeldeseite und erweiterte " +"CSS-Überschreibungen für luci-theme-fluent." + +msgid "Confirm" +msgstr "Bestätigen" + +msgid "Confirm Installation" +msgstr "Installation bestätigen" + +msgid "Continue" +msgstr "Fortfahren" + +msgid "Control height" +msgstr "Steuerelementhöhe" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Steuert die Schriftstärke, die für Hauptnavigationsbezeichnungen und " +"zugehörige Theme-Textelemente verwendet wird." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Steuert die gemeinsamen Fluent-Eckenradien, die von Karten, Schaltflächen, " +"Eingabefeldern und verwandten UI-Elementen verwendet werden." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Steuert das gemeinsame Fluent-Übergangsverhalten, das für Menüs, Header und " +"andere Theme-Animationen verwendet wird." + +msgid "Copied" +msgstr "Kopiert" + +msgid "Copy log" +msgstr "Protokoll kopieren" + +msgid "Corner radius" +msgstr "Eckenradius" + +msgid "Current / Pending MAC Address" +msgstr "Aktuelle / Stehende MAC-Adresse" + +msgid "Custom CSS" +msgstr "Benutzerdefiniertes CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Passen Sie den Hintergrund der Anmeldeseite, die Deckkraft von Karten und " +"den Unschärferadius für helle und dunkle Designs an." + +msgid "Dark mode" +msgstr "Dunkles Design" + +msgid "Delete primary menu" +msgstr "Hauptmenü löschen" + +msgid "Disabled" +msgstr "Deaktiviert" + +msgid "Disconnects" +msgstr "Verbindungsabbrüche" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Zeigt den themenbezogenen Ladeindikator am oberen Rand während des " +"Seitenladens und der Übergänge an." + +msgid "Download and Install" +msgstr "Herunterladen und Installieren" + +msgid "Download log" +msgstr "Protokoll herunterladen" + +msgid "Downloaded" +msgstr "Heruntergeladen" + +msgid "Downloading on router" +msgstr "Herunterladen auf dem Router" + +msgid "Drag to move second-level menu" +msgstr "Verschieben, um das Untermenü zu ändern" + +msgid "Drag to reorder primary menu" +msgstr "Verschieben, um die Reihenfolge des Hauptmenüs zu ändern" + +msgid "Drop second-level menus here" +msgstr "Hier können Sie Untermenüs einfügen." + +msgid "Edit" +msgstr "Bearbeiten" + +msgid "Enable page transition animation" +msgstr "Seitenübergangsanimation aktivieren" + +msgid "Enable tab underline animation" +msgstr "Tab-Unterstreichungsanimation aktivieren" + +msgid "Errors" +msgstr "Fehler" + +msgid "Expecting: %s" +msgstr "Erwartet: %s" + +msgid "Extra large (12px)" +msgstr "Sehr groß (12px)" + +msgid "Failed to check for updates" +msgstr "Fehler beim Prüfen auf Updates" + +msgid "Failed to fetch current theme version." +msgstr "Fehler beim Abrufen der aktuellen Theme-Version." + +msgid "Fast" +msgstr "Schnell" + +msgid "Finished" +msgstr "Fertig" + +msgid "Fix primary menu title errors before saving." +msgstr "Fehler im Titel des Hauptmenüs beheben, bevor Sie speichern." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Fluent Design 2 Theme für LuCI" + +msgid "Fluent Theme" +msgstr "Fluent-Theme" + +msgid "Fluent theme settings" +msgstr "Fluent Theme Einstellungen" + +msgid "Follow system" +msgstr "Systemeinstellungen übernehmen" + +msgid "Force dark mode" +msgstr "Dunkles Design erzwingen" + +msgid "Force light mode" +msgstr "Helles Design erzwingen" + +msgid "Force LTR" +msgstr "LTR erzwingen" + +msgid "Force RTL" +msgstr "RTL erzwingen" + +msgid "General" +msgstr "Allgemein" + +msgid "GitHub Token Required" +msgstr "GitHub-Token erforderlich" + +msgid "Header height" +msgstr "Höhe der Kopfzeile" + +msgid "Height of the top header bar in pixels." +msgstr "Höhe der oberen Kopfleiste in Pixeln." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"HEX-Farbe, die als primäre Fluent-Akzentfarbe verwendet wird, wenn die " +"Oberfläche im dunklen Modus dargestellt wird." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"HEX-Farbe, die als primäre Fluent-Akzentfarbe verwendet wird, wenn die " +"Oberfläche im hellen Modus dargestellt wird." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "HEX-Farbe für Container-/Kartenelemente im dunklen Modus." + +msgid "HEX color used for container/card elements in light mode." +msgstr "HEX-Farbe für Container-/Kartenelemente im hellen Modus." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"HEX-Farbe für Fortschrittsbalkenbeschriftungen, wenn die Oberfläche im " +"dunklen Modus dargestellt wird." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"HEX-Farbe für Fortschrittsbalkenbeschriftungen, wenn die Oberfläche im " +"hellen Modus dargestellt wird." + +msgid "HEX color used for the main page background in dark mode." +msgstr "HEX-Farbe für den Hauptseitenhintergrund im dunklen Modus." + +msgid "HEX color used for the main page background in light mode." +msgstr "HEX-Farbe für den Hauptseitenhintergrund im hellen Modus." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "HEX-Farbe für die Navigationsleiste im dunklen Modus." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "HEX-Farbe für die Navigationsleiste im hellen Modus." + +msgid "Installation cancelled by user." +msgstr "Installation durch Benutzer abgebrochen." + +msgid "Installed version" +msgstr "Installierte Version" + +msgid "Installing package" +msgstr "Paket wird installiert" + +msgid "Keep action bar visible while scrolling" +msgstr "Aktionen während des Scrollens sichtbar halten" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Speichern, Anwenden und Zurücksetzen: Aktionen am unteren Bildschirmrand " +"fixieren, auch beim Scrollen." + +msgid "Large" +msgstr "Groß" + +msgid "Large (8px)" +msgstr "Groß (8px)" + +msgid "Light mode" +msgstr "Helles Design" + +msgid "Line %d" +msgstr "Zeile %d" + +msgid "Lines" +msgstr "Zeilen" + +msgid "Login" +msgstr "Anmelden" + +msgid "Login card opacity" +msgstr "Deckkraft der Anmeldekarte" + +msgid "Login page" +msgstr "Anmeldeseite" + +msgid "Medium" +msgstr "Mittel" + +msgid "Medium (4px)" +msgstr "Mittel (4px)" + +msgid "Menu" +msgstr "Menü" + +msgid "Menu title cannot be empty." +msgstr "Der Menütitel darf nicht leer sein." + +msgid "Navigation font weight" +msgstr "Schriftstärke der Navigation" + +msgid "New primary menu" +msgstr "Neues Hauptmenü" + +msgid "New primary menu %d" +msgstr "Neues Hauptmenü %d" + +msgid "Next" +msgstr "Weiter" + +msgid "Nightly build available (reinstallation check)." +msgstr "Nightly-Build verfügbar (Prüfung auf Neuinstallation)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Nightly-Kanal (Vorabversion)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" +"Keine passende Paketdatei für Ihre Systemarchitektur in dieser Version " +"gefunden." + +msgid "None" +msgstr "Kein" + +msgid "Normal" +msgstr "Normal" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Deckkraft der Anmeldekarte im dunklen Modus. 0 ist vollständig transparent, " +"1 ist vollständig undurchsichtig." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Deckkraft der Anmeldekarte im hellen Modus. 0 ist vollständig transparent, 1 " +"ist vollständig undurchsichtig." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Optionales CSS, das in die Fluent-Header-Vorlage eingefügt wird. Verwenden " +"Sie dies für zusätzliche Überschreibungen von `--fluent-*`-Variablen oder " +"seitenbezogene Anpassungen, die nicht als dedizierte Optionen verfügbar " +"sind." + +msgid "Package format" +msgstr "Paketformat" + +msgid "Page background" +msgstr "Hintergrundfarbe der Seite" + +msgid "Please enter your password." +msgstr "Bitte geben Sie Ihr Passwort ein." + +msgid "Please enter your username." +msgstr "Bitte geben Sie Ihren Benutzernamen ein." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Position der Fortschrittsbalken-Beschriftungen relativ zum Balken. Die " +"Ausrichtung (Anfang/Ende) folgt der Textrichtung (LTR oder RTL)." + +msgid "Preset Vendor / Generator" +msgstr "Vordefinierter Anbieter / Generator" + +msgid "Primary menu title" +msgstr "Titel des Hauptmenüs" + +msgid "Primary menu titles cannot be empty." +msgstr "Die Titel im Hauptmenü dürfen nicht leer sein." + +msgid "Primary menu titles must be unique." +msgstr "Die Titel im Hauptmenü müssen eindeutig sein." + +msgid "Progress bar text color" +msgstr "Textfarbe der Fortschrittsanzeige" + +msgid "Progress bar text position" +msgstr "Textposition des Fortschrittsbalkens" + +msgid "Random Local MAC" +msgstr "Zufällige lokale MAC-Adresse" + +msgid "Randomize MAC" +msgstr "MAC-Adresse zufällig generieren" + +msgid "Reload Web Interface" +msgstr "Weboberfläche neu laden" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" +"Native Select-Elemente durch das benutzerdefinierte Dropdown-Widget des " +"Themes ersetzen." + +msgid "Respect reduced-motion preference" +msgstr "Reduzierte Bewegungseffekte berücksichtigen" + +msgid "Restore %s to its original menu name" +msgstr "%s auf den ursprünglichen Menünamen zurücksetzen" + +msgid "Restore %s to its original menu position" +msgstr "%s auf die ursprüngliche Menüposition zurücksetzen" + +msgid "Restore defaults" +msgstr "Standardwerte wiederherstellen" + +msgid "Restore to original title" +msgstr "Auf ursprünglichen Titel zurücksetzen" + +msgid "Retrying with token..." +msgstr "Erneuter Versuch mit Token..." + +msgid "Rounded (6px)" +msgstr "Abgerundet (6px)" + +msgid "Save" +msgstr "Speichern" + +msgid "Scroll to bottom" +msgstr "Zum Ende scrollen" + +msgid "Scroll to top" +msgstr "Zum Anfang scrollen" + +msgid "Search menu items" +msgstr "Menüpunkte suchen" + +msgid "Search menu..." +msgstr "Menü suchen..." + +msgid "Second-level menu title" +msgstr "Titel des Untermenüs" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "Wählen Sie die Schattenintensität für thematisierte Karten und Panels." + +msgid "Select vendor prefix..." +msgstr "Hersteller-Präfix auswählen..." + +msgid "Semibold" +msgstr "Halbfett" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Legt die Basisschriftgröße der Benutzeroberfläche in Pixeln fest. Die " +"meisten Texte des Themes werden über die Fluent CSS-Variablen von diesem " +"Wert aus skaliert. Empfohlener Bereich: 12-18px." + +msgid "Show %s in the menu" +msgstr "%s im Menü anzeigen" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Zeigt ausstehende UCI-Änderungen in der oberen Statusmeldung an, bis die " +"Konfigurationsänderung abgeschlossen ist." + +msgid "Show top loading bar" +msgstr "Ladebalken oben anzeigen" + +msgid "Show UCI changes while applying configuration" +msgstr "UCI-Änderungen während des Konfigurationsvorgangs anzeigen" + +msgid "Sidebar background" +msgstr "Hintergrund der Seitenleiste" + +msgid "Sidebar width" +msgstr "Breite der Seitenleiste" + +msgid "Slow" +msgstr "Langsam" + +msgid "Small" +msgstr "Klein" + +msgid "Small (2px)" +msgstr "Klein (2px)" + +msgid "Software Update" +msgstr "Software-Update" + +msgid "Source code" +msgstr "Quellcode" + +msgid "Square (0px)" +msgstr "Quadratisch (0px)" + +msgid "Stable Channel" +msgstr "Stabiler Kanal" + +msgid "Starting backend download..." +msgstr "Backend-Download wird gestartet..." + +msgid "Starting update process..." +msgstr "Update-Prozess wird gestartet..." + +msgid "Submit" +msgstr "Absenden" + +msgid "Switch theme mode" +msgstr "Theme-Modus wechseln" + +msgid "Text direction" +msgstr "Textrichtung" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"Das gespeicherte Menülayout ist ungültig. Wählen Sie „Standardwerte " +"wiederherstellen“ oder nehmen Sie eine Änderung vor, bevor Sie speichern." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"Das gespeicherte Menülayout ist ungültig. Stellen Sie die Standardwerte " +"wieder her oder bearbeiten Sie das Layout, bevor Sie speichern." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Das Theme wird auf Ihrem Router installiert. Die Protokolle werden unten " +"angezeigt. Durch Neuladen der Weboberfläche werden die Änderungen " +"angewendet, sobald RPCD neu gestartet ist." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "" +"Das Theme-Paket ist bereit. Sind Sie sicher, dass Sie mit der Installation " +"fortfahren möchten?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Der Token benötigt KEINE Berechtigungen/Scopes (ein schreibgeschützter " +"öffentlicher Zugriff ist ausreichend)." + +msgid "Theme settings" +msgstr "Theme-Einstellungen" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Theme erfolgreich aktualisiert! RPC-Dienst wird neu geladen, bitte Seite in " +"5 Sekunden aktualisieren." + +msgid "Theme transition speed" +msgstr "Übergangsgeschwindigkeit des Themes" + +msgid "Toggle fullscreen" +msgstr "Vollbildmodus umschalten" + +msgid "Triggering installation on router..." +msgstr "Installation auf dem Router wird gestartet..." + +msgid "Update channel" +msgstr "Aktualisierungskanal" + +msgid "Update failed" +msgstr "Update fehlgeschlagen" + +msgid "Upgrade Successful!" +msgstr "Upgrade erfolgreich!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Benutze benutzerdefinierte Fluent-Auswahllisten" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Verwende die Browser-View-Transition-API, um Navigationen zwischen " +"LuCI-Seiten zu animieren, sofern unterstützt." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Verwende die Systemeinstellung/Browserpräferenz oder zeige das Fluent-Theme " +"immer in einer festen hellen oder dunklen Farbpalette an." + +msgid "Username is required." +msgstr "Benutzername ist erforderlich." + +msgid "valid HEX color value" +msgstr "gültiger HEX-Farbwert" + +msgid "Warnings" +msgstr "Warnungen" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Wenn aktiviert, folgen Fluent-Animationen der Browser- oder " +"Betriebssystempräferenz für reduzierte Bewegung." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Breite der Hauptnavigationsleiste in Pixeln." + +msgid "Word wrap" +msgstr "Textumbruch" + +msgid "You can create a new token at" +msgstr "Sie können einen neuen Token erstellen unter:" + +msgid "Your theme is up to date!" +msgstr "Ihr Theme ist auf dem neuesten Stand!" diff --git a/luci-theme-fluent/po/es/fluent.po b/luci-theme-fluent/po/es/fluent.po new file mode 100644 index 000000000..08f68edfb --- /dev/null +++ b/luci-theme-fluent/po/es/fluent.po @@ -0,0 +1,781 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:37+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: castillofrancodamian\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: es\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" + +msgid "%d second-level menus" +msgstr "%d menús de segundo nivel" + +msgid "A new version is available!" +msgstr "¡Ya está disponible una nueva versión!" + +msgid "About" +msgstr "Acerca de" + +msgid "Above bar, end" +msgstr "Arriba de la barra, fin" + +msgid "Above bar, start" +msgstr "Arriba de la barra, inicio" + +msgid "Accent color" +msgstr "Color de acento" + +msgid "Add primary menu" +msgstr "Añadir menú principal" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Ajuste el diseño, la tipografía, la sincronización de las transiciones, las " +"sombras e inserte variables o reglas CSS personalizadas cuando los controles " +"integrados no sean suficientes." + +msgid "Advanced" +msgstr "Avanzado" + +msgid "Advanced MAC Selector" +msgstr "Selector avanzado de dirección MAC" + +msgid "and %d more..." +msgstr "y %d más..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Animar el subrayado activo al cambiar entre las pestañas nativas de LuCI y " +"los menús de pestañas con temas." + +msgid "Animation" +msgstr "Animación" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Se ha superado el límite de solicitudes de la API. Ingrese un token de " +"GitHub para continuar. Este token solo se utiliza para las solicitudes del " +"frontend y no se guardará en el backend del enrutador." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Se aplica a botones, campos de entrada, menús desplegables y controles de " +"formulario similares estándar en todo el tema." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"¿Está seguro de que desea eliminar %d elementos o reglas de menú " +"inválidos/no instalados?" + +msgid "Author" +msgstr "Autor" + +msgid "Auto-scroll" +msgstr "Desplazamiento automático" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Automático (solo para configuraciones regionales árabe/persa)" + +msgid "Backdrop blur radius" +msgstr "Radio de desenfoque del fondo" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Backend (Aceleración GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "Backend (GitHub oficial)" + +msgid "Base font size" +msgstr "Tamaño de fuente base" + +msgid "Below bar, end" +msgstr "Abajo de la barra, fin" + +msgid "Below bar, start" +msgstr "Abajo de la barra, inicio" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Radio de desenfoque en píxeles detrás de la tarjeta de inicio de sesión en " +"modo oscuro. Use 0 para desactivar el desenfoque." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Radio de desenfoque en píxeles detrás de la tarjeta de inicio de sesión en " +"modo claro. Use 0 para desactivar el desenfoque." + +msgid "Cancel" +msgstr "Cancelar" + +msgid "Card background" +msgstr "Fondo de tarjeta" + +msgid "Card shadow" +msgstr "Sombra de la tarjeta" + +msgid "Check for updates" +msgstr "Verificar si hay actualizaciones" + +msgid "Checking for updates..." +msgstr "Buscando actualizaciones..." + +msgid "Choose color" +msgstr "Elegir color" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Seleccione la dirección de los documentos para las páginas autenticadas y de " +"inicio de sesión. El modo automático resuelve los códigos de configuración " +"regional árabe y persa (ar, ar_*, ar-*, fa, fa_*, fa-*) a RTL y recurre a " +"LTR en caso de que falten o no se reconozcan las configuraciones regionales." + +msgid "Clean up" +msgstr "Limpiar" + +msgid "Clean up invalid items" +msgstr "Limpiar elementos inválidos" + +msgid "Clean up invalid items (%d)" +msgstr "Limpiar elementos inválidos (%d)" + +msgid "Close" +msgstr "Cerrar" + +msgid "Color mode" +msgstr "Modo de color" + +msgid "Colors" +msgstr "Colores" + +msgid "Comfortable (42px)" +msgstr "Cómodo (42px)" + +msgid "Compact (32px)" +msgstr "Compacto (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Configure el modo de color, los colores de acento, el tamaño del diseño, el " +"comportamiento de la animación, la apariencia de la página de inicio de " +"sesión y las anulaciones CSS avanzadas para luci-theme-fluent." + +msgid "Confirm" +msgstr "Confirmar" + +msgid "Confirm Installation" +msgstr "Confirmar instalación" + +msgid "Continue" +msgstr "Continuar" + +msgid "Control height" +msgstr "Altura de control" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Controla el grosor de la fuente utilizada por las etiquetas de navegación " +"principales y los acentos de texto del tema relacionados." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Controla los tokens de radio de esquina compartidos de Fluent que utilizan " +"las tarjetas, los botones, los campos de entrada y las superficies de la " +"interfaz de usuario relacionadas." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Controla la sincronización de transición compartida de Fluent utilizada por " +"las animaciones de menú, encabezado y otros temas." + +msgid "Copied" +msgstr "Copiado" + +msgid "Copy log" +msgstr "Copiar registro" + +msgid "Corner radius" +msgstr "Radio de esquina" + +msgid "Current / Pending MAC Address" +msgstr "Dirección MAC actual / pendiente" + +msgid "Custom CSS" +msgstr "CSS personalizado" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Personalice el fondo de la página de inicio de sesión, la opacidad de la " +"tarjeta y el radio de desenfoque para los modos claro y oscuro." + +msgid "Dark mode" +msgstr "Modo oscuro" + +msgid "Delete primary menu" +msgstr "Eliminar menú principal" + +msgid "Disabled" +msgstr "Desactivado" + +msgid "Disconnects" +msgstr "Desconexiones" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Muestra el indicador de carga con el tema correspondiente en el borde " +"superior durante la carga y las transiciones de la página." + +msgid "Download and Install" +msgstr "Descargar e instalar" + +msgid "Download log" +msgstr "Descargar registro" + +msgid "Downloaded" +msgstr "Descargado" + +msgid "Downloading on router" +msgstr "Descargando en el enrutador" + +msgid "Drag to move second-level menu" +msgstr "Arrastrar para mover menú de segundo nivel" + +msgid "Drag to reorder primary menu" +msgstr "Arrastrar para reordenar menú principal" + +msgid "Drop second-level menus here" +msgstr "Soltar menús de segundo nivel aquí" + +msgid "Edit" +msgstr "Editar" + +msgid "Enable page transition animation" +msgstr "Activar la animación de transición de página" + +msgid "Enable tab underline animation" +msgstr "Activar la animación de subrayado de la pestaña" + +msgid "Errors" +msgstr "Errores" + +msgid "Expecting: %s" +msgstr "Esperando: %s" + +msgid "Extra large (12px)" +msgstr "Extra grande (12 píxeles)" + +msgid "Failed to check for updates" +msgstr "Error al comprobar si hay actualizaciones" + +msgid "Failed to fetch current theme version." +msgstr "Error al obtener la versión actual del tema." + +msgid "Fast" +msgstr "Rápido" + +msgid "Finished" +msgstr "Finalizado" + +msgid "Fix primary menu title errors before saving." +msgstr "Corregir errores en el título del menú principal antes de guardar." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Tema Fluent Design 2 para LuCI" + +msgid "Fluent Theme" +msgstr "Tema Fluent" + +msgid "Fluent theme settings" +msgstr "Configuración del tema Fluent" + +msgid "Follow system" +msgstr "Seguir el sistema" + +msgid "Force dark mode" +msgstr "Forzar modo oscuro" + +msgid "Force light mode" +msgstr "Forzar modo claro" + +msgid "Force LTR" +msgstr "Forzar LTR" + +msgid "Force RTL" +msgstr "Forzar RTL" + +msgid "General" +msgstr "General" + +msgid "GitHub Token Required" +msgstr "Se requiere token de GitHub" + +msgid "Header height" +msgstr "Altura del encabezado" + +msgid "Height of the top header bar in pixels." +msgstr "Altura de la barra de encabezado superior en píxeles." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Se utiliza un color HEX como acento principal de Fluent cuando la interfaz " +"se muestra en modo oscuro." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Se utiliza un color HEX como acento principal de Fluent cuando la interfaz " +"se muestra en modo claro." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "Color HEX utilizado para los elementos de contenedor/tarjeta en modo oscuro." + +msgid "HEX color used for container/card elements in light mode." +msgstr "Color HEX utilizado para los elementos de contenedor/tarjeta en modo claro." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"Se utiliza un código de color hexadecimal para las etiquetas de la barra de " +"progreso cuando la interfaz se muestra en modo oscuro." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Se utiliza un código de color hexadecimal para las etiquetas de la barra de " +"progreso cuando la interfaz se muestra en modo claro." + +msgid "HEX color used for the main page background in dark mode." +msgstr "Color HEX utilizado para el fondo de la página principal en modo oscuro." + +msgid "HEX color used for the main page background in light mode." +msgstr "Color HEX utilizado para el fondo de la página principal en modo claro." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "Color HEX utilizado para la barra lateral de navegación en modo oscuro." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "Color HEX utilizado para la barra lateral de navegación en modo claro." + +msgid "Installation cancelled by user." +msgstr "Instalación cancelada por el usuario." + +msgid "Installed version" +msgstr "Versión instalada" + +msgid "Installing package" +msgstr "Instalando paquete" + +msgid "Keep action bar visible while scrolling" +msgstr "Mantener la barra de acciones visible al hacer desplazamiento." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Mantener los botones Guardar, Aplicar y Restablecer fijos en la parte " +"inferior de la pantalla al hacer desplazamiento." + +msgid "Large" +msgstr "Grande" + +msgid "Large (8px)" +msgstr "Grande (8 píxeles)" + +msgid "Light mode" +msgstr "Modo claro" + +msgid "Line %d" +msgstr "Línea %d" + +msgid "Lines" +msgstr "Líneas" + +msgid "Login" +msgstr "Acceso" + +msgid "Login card opacity" +msgstr "Opacidad de la tarjeta de inicio de sesión" + +msgid "Login page" +msgstr "Página de inicio de sesión" + +msgid "Medium" +msgstr "Mediano" + +msgid "Medium (4px)" +msgstr "Mediano (4px)" + +msgid "Menu" +msgstr "Menú" + +msgid "Menu title cannot be empty." +msgstr "El título del menú no puede estar vacío." + +msgid "Navigation font weight" +msgstr "Peso de la fuente de navegación" + +msgid "New primary menu" +msgstr "Nuevo menú principal" + +msgid "New primary menu %d" +msgstr "Nuevo menú principal %d" + +msgid "Next" +msgstr "Siguiente" + +msgid "Nightly build available (reinstallation check)." +msgstr "Versión Nightly disponible (comprobación de reinstalación)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Canal Nightly (Lanzamiento anticipado)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" +"No se encontró ningún paquete compatible con la arquitectura de su sistema " +"en esta versión." + +msgid "None" +msgstr "Ninguno" + +msgid "Normal" +msgstr "Normal" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Opacidad de la tarjeta de inicio de sesión en modo oscuro. 0 es totalmente " +"transparente y 1 es totalmente opaco." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Opacidad de la tarjeta de inicio de sesión en modo claro. 0 es totalmente " +"transparente y 1 es totalmente opaco." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"CSS sin formato opcional que se inyecta en la plantilla de encabezado de " +"Fluent. Úselo para anular variables adicionales de --fluent-* o para " +"realizar ajustes específicos de la página que no estén disponibles como " +"opciones dedicadas." + +msgid "Package format" +msgstr "Formato del paquete" + +msgid "Page background" +msgstr "Fondo de página" + +msgid "Please enter your password." +msgstr "Por favor, ingrese su contraseña." + +msgid "Please enter your username." +msgstr "Por favor, ingrese su nombre de usuario." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Posición de las etiquetas de la barra de progreso en relación con la barra. " +"La alineación de inicio/fin sigue la dirección del texto (LTR o RTL)." + +msgid "Preset Vendor / Generator" +msgstr "Generador/preajuste de fabricante" + +msgid "Primary menu title" +msgstr "Título del menú principal" + +msgid "Primary menu titles cannot be empty." +msgstr "Los títulos del menú principal no pueden estar vacíos." + +msgid "Primary menu titles must be unique." +msgstr "Los títulos del menú principal deben ser únicos." + +msgid "Progress bar text color" +msgstr "Color del texto de la barra de progreso" + +msgid "Progress bar text position" +msgstr "Posición del texto de la barra de progreso" + +msgid "Random Local MAC" +msgstr "MAC local aleatorio" + +msgid "Randomize MAC" +msgstr "Aleatorizar MAC" + +msgid "Reload Web Interface" +msgstr "Recargar la interfaz web" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" +"Reemplazar los elementos select nativos con el widget desplegable " +"personalizado del tema." + +msgid "Respect reduced-motion preference" +msgstr "Respetar la preferencia por movimientos reducidos" + +msgid "Restore %s to its original menu name" +msgstr "Restaurar %s a su nombre de menú original" + +msgid "Restore %s to its original menu position" +msgstr "Restaurar %s a su posición original en el menú" + +msgid "Restore defaults" +msgstr "Restablecer valores predeterminados" + +msgid "Restore to original title" +msgstr "Restaurar al título original" + +msgid "Retrying with token..." +msgstr "Reintentando con token..." + +msgid "Rounded (6px)" +msgstr "Redondeado (6px)" + +msgid "Save" +msgstr "Guardar" + +msgid "Scroll to bottom" +msgstr "Desplazarse al final" + +msgid "Scroll to top" +msgstr "Desplazarse a la parte superior" + +msgid "Search menu items" +msgstr "Buscar elementos del menú" + +msgid "Search menu..." +msgstr "Menú de búsqueda..." + +msgid "Second-level menu title" +msgstr "Título de menú secundario" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "" +"Seleccione la profundidad de sombra que se aplicará a las tarjetas y paneles " +"temáticos." + +msgid "Select vendor prefix..." +msgstr "Seleccionar prefijo de fabricante..." + +msgid "Semibold" +msgstr "Seminegrita" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Establezca el tamaño de fuente base de la interfaz en píxeles. La mayoría " +"del texto del tema se ajusta a partir de este valor mediante las variables " +"CSS de Fluent. Rango recomendado: 12-18 píxeles." + +msgid "Show %s in the menu" +msgstr "Mostrar %s en el menú" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Mostrar cambios de UCI pendientes en el mensaje superior de estado de " +"aplicación hasta que se complete la aplicación de la configuración." + +msgid "Show top loading bar" +msgstr "Mostrar barra de carga superior" + +msgid "Show UCI changes while applying configuration" +msgstr "Mostrar cambios de UCI durante la aplicación de la configuración." + +msgid "Sidebar background" +msgstr "Fondo de la barra lateral" + +msgid "Sidebar width" +msgstr "Ancho de la barra lateral" + +msgid "Slow" +msgstr "Lento" + +msgid "Small" +msgstr "Pequeño" + +msgid "Small (2px)" +msgstr "Pequeño (2px)" + +msgid "Software Update" +msgstr "Actualización de software" + +msgid "Source code" +msgstr "Código fuente" + +msgid "Square (0px)" +msgstr "Cuadrado (0px)" + +msgid "Stable Channel" +msgstr "Canal estable" + +msgid "Starting backend download..." +msgstr "Iniciando la descarga del servidor..." + +msgid "Starting update process..." +msgstr "Iniciando el proceso de actualización..." + +msgid "Submit" +msgstr "Enviar" + +msgid "Switch theme mode" +msgstr "Cambiar modo del tema" + +msgid "Text direction" +msgstr "Dirección del texto" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"La disposición del menú almacenada no es válida. Elija \"Restablecer valores " +"predeterminados\" o realice una edición antes de guardar." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"La disposición del menú almacenada no es válida. Restablezca los valores " +"predeterminados o edite la disposición antes de guardar." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"El tema se está instalando en su enrutador. Los registros se muestran a " +"continuación. Al reiniciar RPCD, se aplicarán los cambios a la interfaz web." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "" +"El paquete de temas está listo. ¿Está seguro que desea continuar con la " +"instalación?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"El token NO requiere que se otorguen permisos/ámbitos (el acceso público de " +"solo lectura es suficiente)." + +msgid "Theme settings" +msgstr "Configuración del tema" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"¡Tema actualizado correctamente! Recargando el servicio RPC, y actualice la " +"página en 5 segundos." + +msgid "Theme transition speed" +msgstr "Velocidad de transición del tema" + +msgid "Toggle fullscreen" +msgstr "Alternar pantalla completa" + +msgid "Triggering installation on router..." +msgstr "Iniciando la instalación en el enrutador..." + +msgid "Update channel" +msgstr "Canal de actualización" + +msgid "Update failed" +msgstr "La actualización falló" + +msgid "Upgrade Successful!" +msgstr "¡Actualización exitosa!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Utilizar menús desplegables de selección personalizada de Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Utiliza la API de transición de vista del navegador para animar la " +"navegación entre páginas de LuCI cuando sea compatible." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Se utiliza la preferencia del sistema/navegador o configure que el tema " +"Fluent se muestre siempre con una paleta de colores fija, ya sea clara u " +"oscura." + +msgid "Username is required." +msgstr "Se requiere nombre de usuario." + +msgid "valid HEX color value" +msgstr "valor de color HEX válido" + +msgid "Warnings" +msgstr "Advertencias" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Cuando están activadas, las animaciones Fluent respetan la preferencia de " +"movimiento reducido del navegador o del sistema operativo." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Ancho de la barra lateral de navegación principal en píxeles." + +msgid "Word wrap" +msgstr "Ajuste de línea" + +msgid "You can create a new token at" +msgstr "Puedes crear un nuevo token en:" + +msgid "Your theme is up to date!" +msgstr "¡Tu tema está actualizado!" diff --git a/luci-theme-fluent/po/fa/fluent.po b/luci-theme-fluent/po/fa/fluent.po new file mode 100644 index 000000000..576674206 --- /dev/null +++ b/luci-theme-fluent/po/fa/fluent.po @@ -0,0 +1,761 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:37+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: fa\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" + +msgid "%d second-level menus" +msgstr "%d منوی سطح دوم" + +msgid "A new version is available!" +msgstr "نسخه جدیدی در دسترس است!" + +msgid "About" +msgstr "درباره" + +msgid "Above bar, end" +msgstr "بالای نوار، پایان" + +msgid "Above bar, start" +msgstr "بالای نوار، شروع" + +msgid "Accent color" +msgstr "رنگ تأکیدی" + +msgid "Add primary menu" +msgstr "افزودن منوی اصلی" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"چیدمان، تایپوگرافی، زمان‌بندی گذارها و سایه‌ها را تنظیم کنید و وقتی " +"کنترل‌های داخلی کافی نیستند، متغیرها یا قوانین CSS سفارشی اضافه کنید." + +msgid "Advanced" +msgstr "پیشرفته" + +msgid "Advanced MAC Selector" +msgstr "انتخابگر پیشرفته آدرس MAC" + +msgid "and %d more..." +msgstr "و %d مورد دیگر..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"هنگام جابه‌جایی بین زبانه‌های بومی LuCI و منوهای زبانه‌ای پوسته، زیرخط فعال " +"را متحرک کنید." + +msgid "Animation" +msgstr "انیمیشن" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"محدودیت نرخ API پر شده است. برای ادامه، یک توکن GitHub وارد کنید. این توکن " +"فقط برای درخواست‌های فرانت‌اند استفاده می‌شود و در سمت روتر ذخیره نخواهد شد." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"برای دکمه‌ها، ورودی‌ها، فهرست‌های انتخاب و کنترل‌های مشابه فرم در سراسر " +"پوسته اعمال می‌شود." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"آیا مطمئن هستید که می‌خواهید %d مورد نامعتبر یا غیرنصب‌شده از منوها یا " +"قوانین را حذف کنید؟" + +msgid "Author" +msgstr "نویسنده" + +msgid "Auto-scroll" +msgstr "اسکرول خودکار" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "خودکار (فقط برای زبان‌های عربی/پارسی)" + +msgid "Backdrop blur radius" +msgstr "شعاع تاری پس‌زمینه" + +msgid "Backend (GHProxy Acceleration)" +msgstr "بک‌اند (شتاب‌دهی GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "بک‌اند (GitHub رسمی)" + +msgid "Base font size" +msgstr "اندازه پایه فونت" + +msgid "Below bar, end" +msgstr "زیر نوار، پایان" + +msgid "Below bar, start" +msgstr "زیر نوار، شروع" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"شعاع تاری بر حسب پیکسل پشت کارت ورود در حالت تیره. برای غیرفعال کردن تاری از " +"0 استفاده کنید." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"شعاع تاری بر حسب پیکسل پشت کارت ورود در حالت روشن. برای غیرفعال کردن تاری از " +"0 استفاده کنید." + +msgid "Cancel" +msgstr "انصراف" + +msgid "Card background" +msgstr "پس‌زمینه کارت" + +msgid "Card shadow" +msgstr "سایه کارت" + +msgid "Check for updates" +msgstr "بررسی به‌روزرسانی‌ها" + +msgid "Checking for updates..." +msgstr "در حال بررسی به‌روزرسانی‌ها..." + +msgid "Choose color" +msgstr "انتخاب رنگ" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"جهت متن را برای صفحات احراز هویت و ورود انتخاب کنید. حالت خودکار، کدهای زبان " +"عربی و پارسی (ar, ar_*, ar-*, fa, fa_*, fa-*) را شناسایی کرده و آن‌ها را به " +"راست به چپ (RTL) تنظیم می‌کند و در صورت عدم وجود یا نامشخص بودن کد زبان، به " +"چپ به راست (LTR) بازمی‌گردد." + +msgid "Clean up" +msgstr "پاکسازی" + +msgid "Clean up invalid items" +msgstr "پاکسازی موارد نامعتبر" + +msgid "Clean up invalid items (%d)" +msgstr "پاکسازی موارد نامعتبر (%d)" + +msgid "Close" +msgstr "بستن" + +msgid "Color mode" +msgstr "حالت رنگ" + +msgid "Colors" +msgstr "رنگ‌ها" + +msgid "Comfortable (42px)" +msgstr "راحت (42px)" + +msgid "Compact (32px)" +msgstr "فشرده (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"حالت رنگ، رنگ‌های تأکیدی، اندازه‌بندی چیدمان، رفتار انیمیشن، ظاهر صفحه ورود " +"و بازنویسی‌های پیشرفته CSS برای luci-theme-fluent را پیکربندی کنید." + +msgid "Confirm" +msgstr "تایید" + +msgid "Confirm Installation" +msgstr "تأیید نصب" + +msgid "Continue" +msgstr "ادامه" + +msgid "Control height" +msgstr "ارتفاع کنترل‌ها" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"ضخامت فونت مورد استفاده برای برچسب‌های اصلی ناوبری و متن‌های تأکیدی مرتبط با " +"پوسته را کنترل می‌کند." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"مقادیر مشترک شعاع گوشه Fluent را که در کارت‌ها، دکمه‌ها، ورودی‌ها و سطوح " +"مرتبط رابط کاربری استفاده می‌شوند کنترل می‌کند." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"زمان‌بندی گذار مشترک Fluent را که در منو، سربرگ و دیگر انیمیشن‌های پوسته " +"استفاده می‌شود کنترل می‌کند." + +msgid "Copied" +msgstr "کپی شد" + +msgid "Copy log" +msgstr "کپی لاگ" + +msgid "Corner radius" +msgstr "شعاع گوشه" + +msgid "Current / Pending MAC Address" +msgstr "آدرس MAC فعلی / در انتظار" + +msgid "Custom CSS" +msgstr "CSS سفارشی" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"پس‌زمینه صفحه ورود، شفافیت کارت و شعاع تاری را برای حالت‌های روشن و تیره " +"سفارشی کنید." + +msgid "Dark mode" +msgstr "حالت تیره" + +msgid "Delete primary menu" +msgstr "حذف منوی اصلی" + +msgid "Disabled" +msgstr "غیرفعال" + +msgid "Disconnects" +msgstr "قطع شدن‌ها" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"هنگام بارگذاری و گذار صفحات، نشانگر بارگذاری پوسته را در لبه بالایی نمایش " +"می‌دهد." + +msgid "Download and Install" +msgstr "دانلود و نصب" + +msgid "Download log" +msgstr "دانلود لاگ" + +msgid "Downloaded" +msgstr "دانلود شد" + +msgid "Downloading on router" +msgstr "در حال دانلود روی روتر" + +msgid "Drag to move second-level menu" +msgstr "برای جابجایی، منوی سطح دوم را بکشید" + +msgid "Drag to reorder primary menu" +msgstr "برای تغییر ترتیب، منوی اصلی را بکشید" + +msgid "Drop second-level menus here" +msgstr "منوهای سطح دوم را در اینجا رها کنید" + +msgid "Edit" +msgstr "ویرایش" + +msgid "Enable page transition animation" +msgstr "فعال‌سازی انیمیشن گذار صفحه" + +msgid "Enable tab underline animation" +msgstr "فعال‌سازی انیمیشن زیرخط زبانه" + +msgid "Errors" +msgstr "خطاها" + +msgid "Expecting: %s" +msgstr "مورد انتظار: %s" + +msgid "Extra large (12px)" +msgstr "بسیار بزرگ (12px)" + +msgid "Failed to check for updates" +msgstr "بررسی به‌روزرسانی‌ها ناموفق بود" + +msgid "Failed to fetch current theme version." +msgstr "واکشی نسخه فعلی پوسته ناموفق بود." + +msgid "Fast" +msgstr "سریع" + +msgid "Finished" +msgstr "تکمیل شد" + +msgid "Fix primary menu title errors before saving." +msgstr "قبل از ذخیره، خطاهای عنوان منوی اصلی را برطرف کنید." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "پوسته Fluent Design 2 برای LuCI" + +msgid "Fluent Theme" +msgstr "پوسته Fluent" + +msgid "Fluent theme settings" +msgstr "تنظیمات پوسته Fluent" + +msgid "Follow system" +msgstr "پیروی از سیستم" + +msgid "Force dark mode" +msgstr "اجبار به حالت تیره" + +msgid "Force light mode" +msgstr "اجبار به حالت روشن" + +msgid "Force LTR" +msgstr "اجبار به چپ به راست (LTR)" + +msgid "Force RTL" +msgstr "اجبار به راست به چپ (RTL)" + +msgid "General" +msgstr "عمومی" + +msgid "GitHub Token Required" +msgstr "توکن GitHub لازم است" + +msgid "Header height" +msgstr "ارتفاع سربرگ" + +msgid "Height of the top header bar in pixels." +msgstr "ارتفاع نوار سربرگ بالا بر حسب پیکسل." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"رنگ HEX که هنگام نمایش رابط در حالت تیره به‌عنوان رنگ تأکیدی اصلی Fluent " +"استفاده می‌شود." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"رنگ HEX که هنگام نمایش رابط در حالت روشن به‌عنوان رنگ تأکیدی اصلی Fluent " +"استفاده می‌شود." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "رنگ HEX مورد استفاده برای عناصر ظرف/کارت در حالت تیره." + +msgid "HEX color used for container/card elements in light mode." +msgstr "رنگ HEX مورد استفاده برای عناصر ظرف/کارت در حالت روشن." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"رنگ HEX مورد استفاده برای برچسب‌های نوار پیشرفت هنگام نمایش رابط در حالت " +"تیره." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"رنگ HEX مورد استفاده برای برچسب‌های نوار پیشرفت هنگام نمایش رابط در حالت " +"روشن." + +msgid "HEX color used for the main page background in dark mode." +msgstr "رنگ HEX مورد استفاده برای پس‌زمینه صفحه اصلی در حالت تیره." + +msgid "HEX color used for the main page background in light mode." +msgstr "رنگ HEX مورد استفاده برای پس‌زمینه صفحه اصلی در حالت روشن." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "رنگ HEX مورد استفاده برای نوار کناری ناوبری در حالت تیره." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "رنگ HEX مورد استفاده برای نوار کناری ناوبری در حالت روشن." + +msgid "Installation cancelled by user." +msgstr "نصب توسط کاربر لغو شد." + +msgid "Installed version" +msgstr "نسخه نصب‌شده" + +msgid "Installing package" +msgstr "در حال نصب بسته" + +msgid "Keep action bar visible while scrolling" +msgstr "نوار عملگرها را هنگام پیمایش، قابل مشاهده نگه دارید." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"دکمه‌های ذخیره، اعمال و بازنشانی را در پایین صفحه ثابت نگه دارید تا هنگام " +"پیمایش همیشه قابل دسترس باشند." + +msgid "Large" +msgstr "بزرگ" + +msgid "Large (8px)" +msgstr "بزرگ (8px)" + +msgid "Light mode" +msgstr "حالت روشن" + +msgid "Line %d" +msgstr "سطر %d" + +msgid "Lines" +msgstr "سطرها" + +msgid "Login" +msgstr "ورود" + +msgid "Login card opacity" +msgstr "شفافیت کارت ورود" + +msgid "Login page" +msgstr "صفحه ورود" + +msgid "Medium" +msgstr "متوسط" + +msgid "Medium (4px)" +msgstr "متوسط (4px)" + +msgid "Menu" +msgstr "منو" + +msgid "Menu title cannot be empty." +msgstr "عنوان منو نمی‌تواند خالی باشد." + +msgid "Navigation font weight" +msgstr "ضخامت فونت ناوبری" + +msgid "New primary menu" +msgstr "منوی اصلی جدید" + +msgid "New primary menu %d" +msgstr "منوی اصلی جدید %d" + +msgid "Next" +msgstr "بعدی" + +msgid "Nightly build available (reinstallation check)." +msgstr "بیلد Nightly در دسترس است (بررسی نیاز به نصب مجدد)." + +msgid "Nightly Channel (Prerelease)" +msgstr "کانال Nightly (پیش‌انتشار)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "هیچ بسته سازگار با معماری سیستم شما در این انتشار پیدا نشد." + +msgid "None" +msgstr "هیچ‌کدام" + +msgid "Normal" +msgstr "عادی" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "شفافیت کارت ورود در حالت تیره. 0 یعنی کاملاً شفاف و 1 یعنی کاملاً مات." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "شفافیت کارت ورود در حالت روشن. 0 یعنی کاملاً شفاف و 1 یعنی کاملاً مات." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"CSS خام اختیاری که به قالب سربرگ Fluent تزریق می‌شود. از آن برای " +"بازنویسی‌های اضافی متغیرهای --fluent-* یا تنظیمات مخصوص صفحه که به‌صورت " +"گزینه جداگانه ارائه نشده‌اند استفاده کنید." + +msgid "Package format" +msgstr "قالب بسته" + +msgid "Page background" +msgstr "پس‌زمینه صفحه" + +msgid "Please enter your password." +msgstr "لطفاً گذرواژه خود را وارد کنید." + +msgid "Please enter your username." +msgstr "لطفاً نام کاربری خود را وارد کنید." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"موقعیت برچسب‌های نوار پیشرفت نسبت به نوار. تراز شروع/پایان از جهت متن (LTR " +"یا RTL) پیروی می‌کند." + +msgid "Preset Vendor / Generator" +msgstr "تنظیمات پیش‌فرض فروشنده / تولیدکننده" + +msgid "Primary menu title" +msgstr "عنوان منوی اصلی" + +msgid "Primary menu titles cannot be empty." +msgstr "عنوان‌های منوی اصلی نمی‌توانند خالی باشند." + +msgid "Primary menu titles must be unique." +msgstr "عنوان‌های منوی اصلی باید منحصر به فرد باشند." + +msgid "Progress bar text color" +msgstr "رنگ متن نوار پیشرفت" + +msgid "Progress bar text position" +msgstr "موقعیت متن نوار پیشرفت" + +msgid "Random Local MAC" +msgstr "آدرس MAC محلی تصادفی" + +msgid "Randomize MAC" +msgstr "تغییر تصادفی آدرس MAC" + +msgid "Reload Web Interface" +msgstr "بارگذاری مجدد رابط وب" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "عناصر انتخاب بومی را با ابزارک فهرست کشویی سفارشی پوسته جایگزین می‌کند." + +msgid "Respect reduced-motion preference" +msgstr "رعایت ترجیح کاهش حرکت" + +msgid "Restore %s to its original menu name" +msgstr "بازگرداندن %s به نام منوی پیش‌فرض" + +msgid "Restore %s to its original menu position" +msgstr "بازگرداندن %s به موقعیت منوی پیش‌فرض" + +msgid "Restore defaults" +msgstr "بازیابی تنظیمات پیش‌فرض" + +msgid "Restore to original title" +msgstr "بازگرداندن به عنوان اصلی" + +msgid "Retrying with token..." +msgstr "تلاش دوباره با توکن..." + +msgid "Rounded (6px)" +msgstr "گرد (6px)" + +msgid "Save" +msgstr "ذخیره" + +msgid "Scroll to bottom" +msgstr "رفتن به پایین" + +msgid "Scroll to top" +msgstr "رفتن به بالا" + +msgid "Search menu items" +msgstr "جست‌وجوی موارد منو" + +msgid "Search menu..." +msgstr "جست‌وجو در منو..." + +msgid "Second-level menu title" +msgstr "عنوان منوی سطح دوم" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "عمق سایه اعمال‌شده به کارت‌ها و پنل‌های پوسته را انتخاب کنید." + +msgid "Select vendor prefix..." +msgstr "انتخاب پیشوند سازنده..." + +msgid "Semibold" +msgstr "نیمه‌پررنگ" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"اندازه پایه فونت رابط را بر حسب پیکسل تنظیم می‌کند. بیشتر متن‌های پوسته از " +"این مقدار از طریق متغیرهای CSS مربوط به Fluent مقیاس می‌گیرند. بازه " +"پیشنهادی: 12-18px." + +msgid "Show %s in the menu" +msgstr "نمایش %s در منو" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "نمایش تغییرات تنظیمات باقی‌مانده در پیام وضعیت اصلی تا تکمیل اعمال پیکربندی." + +msgid "Show top loading bar" +msgstr "نمایش نوار بارگذاری بالا" + +msgid "Show UCI changes while applying configuration" +msgstr "نمایش تغییرات تنظیمات در حین اعمال پیکربندی" + +msgid "Sidebar background" +msgstr "پس‌زمینه نوار کناری" + +msgid "Sidebar width" +msgstr "عرض نوار کناری" + +msgid "Slow" +msgstr "کند" + +msgid "Small" +msgstr "کوچک" + +msgid "Small (2px)" +msgstr "کوچک (2px)" + +msgid "Software Update" +msgstr "به‌روزرسانی نرم‌افزار" + +msgid "Source code" +msgstr "کد منبع" + +msgid "Square (0px)" +msgstr "مربع (0px)" + +msgid "Stable Channel" +msgstr "کانال پایدار" + +msgid "Starting backend download..." +msgstr "شروع دانلود در بک‌اند..." + +msgid "Starting update process..." +msgstr "شروع فرایند به‌روزرسانی..." + +msgid "Submit" +msgstr "ارسال" + +msgid "Switch theme mode" +msgstr "تغییر حالت پوسته" + +msgid "Text direction" +msgstr "جهت متن" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"چیدمان منوی ذخیره شده نامعتبر است. از «بازیابی تنظیمات پیش‌فرض» استفاده کنید " +"یا قبل از ذخیره، ویرایشی انجام دهید." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"چیدمان منوی ذخیره شده نامعتبر است. تنظیمات پیش‌فرض را بازیابی کنید یا قبل از " +"ذخیره، چیدمان را ویرایش کنید." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"پوسته در حال نصب روی روتر شما است. گزارش‌ها در پایین نمایش داده می‌شوند. پس " +"از راه‌اندازی مجدد RPCD، با بارگذاری مجدد رابط وب تغییرات اعمال می‌شوند." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "بسته پوسته آماده است. آیا مطمئن هستید که می‌خواهید نصب را ادامه دهید؟" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"این توکن به هیچ مجوز یا محدوده دسترسی اضافه‌ای نیاز ندارد (دسترسی عمومی " +"فقط‌خواندنی کافی است)." + +msgid "Theme settings" +msgstr "تنظیمات پوسته" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"پوسته با موفقیت به‌روزرسانی شد! سرویس RPC در حال بارگذاری مجدد است، لطفاً 5 " +"ثانیه دیگر صفحه را تازه‌سازی کنید." + +msgid "Theme transition speed" +msgstr "سرعت گذار پوسته" + +msgid "Toggle fullscreen" +msgstr "تغییر وضعیت تمام صفحه" + +msgid "Triggering installation on router..." +msgstr "در حال آغاز نصب روی روتر..." + +msgid "Update channel" +msgstr "کانال به‌روزرسانی" + +msgid "Update failed" +msgstr "به‌روزرسانی ناموفق بود" + +msgid "Upgrade Successful!" +msgstr "ارتقا با موفقیت انجام شد!" + +msgid "Use Fluent custom select dropdowns" +msgstr "استفاده از فهرست‌های کشویی سفارشی Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"در صورت پشتیبانی، از View Transition API مرورگر برای انیمیشن ناوبری بین " +"صفحات LuCI استفاده کنید." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"از ترجیح سیستم/مرورگر استفاده کنید یا همیشه پوسته Fluent را با یک پالت ثابت " +"روشن یا تیره نمایش دهید." + +msgid "Username is required." +msgstr "نام کاربری الزامی است." + +msgid "valid HEX color value" +msgstr "مقدار معتبر رنگ HEX" + +msgid "Warnings" +msgstr "هشدارها" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"اگر فعال باشد، انیمیشن‌های Fluent از ترجیح کاهش حرکت مرورگر یا سیستم‌عامل " +"پیروی می‌کنند." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "عرض نوار کناری اصلی ناوبری بر حسب پیکسل." + +msgid "Word wrap" +msgstr "پیچش متن" + +msgid "You can create a new token at" +msgstr "می‌توانید یک توکن جدید در اینجا ایجاد کنید:" + +msgid "Your theme is up to date!" +msgstr "پوسته شما به‌روز است!" diff --git a/luci-theme-fluent/po/fr/fluent.po b/luci-theme-fluent/po/fr/fluent.po new file mode 100644 index 000000000..acaa7a558 --- /dev/null +++ b/luci-theme-fluent/po/fr/fluent.po @@ -0,0 +1,781 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: fr\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" + +msgid "%d second-level menus" +msgstr "%d menus de second niveau" + +msgid "A new version is available!" +msgstr "Une nouvelle version est disponible !" + +msgid "About" +msgstr "À propos" + +msgid "Above bar, end" +msgstr "Fin de la barre, en haut" + +msgid "Above bar, start" +msgstr "Début de la barre, en haut" + +msgid "Accent color" +msgstr "Couleur d'accentuation" + +msgid "Add primary menu" +msgstr "Ajouter un menu principal" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Ajustez la mise en page, la typographie, les animations, les ombres et " +"injectez des variables CSS personnalisées ou des règles lorsque les " +"contrôles intégrés ne suffisent pas." + +msgid "Advanced" +msgstr "Avancé" + +msgid "Advanced MAC Selector" +msgstr "Sélectionneur MAC avancé" + +msgid "and %d more..." +msgstr "et %d autres..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Animez le soulignement actif lors du passage entre les onglets natifs de " +"LuCI et les menus d'onglets thématiques." + +msgid "Animation" +msgstr "Animation" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Limite de requêtes API dépassée. Veuillez entrer un jeton GitHub pour " +"continuer. Ce jeton est utilisé uniquement pour les requêtes du frontend et " +"ne sera pas enregistré sur le serveur du routeur." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"S'applique aux boutons, champs de saisie, menus déroulants et autres " +"éléments de formulaire standard dans le thème." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Êtes-vous sûr de vouloir supprimer %d éléments de menu ou règles " +"invalides/non installés ?" + +msgid "Author" +msgstr "Auteur" + +msgid "Auto-scroll" +msgstr "Défilement automatique" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Automatique (uniquement pour les langues arabes/persanes)" + +msgid "Backdrop blur radius" +msgstr "Rayon du flou d'arrière-plan" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Serveur (Accélération GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "Backend (GitHub officiel)" + +msgid "Base font size" +msgstr "Taille de police de base" + +msgid "Below bar, end" +msgstr "Fin de la barre, en bas" + +msgid "Below bar, start" +msgstr "Début de la barre, en bas" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Rayon du flou en pixels derrière la carte de connexion en mode sombre. " +"Utilisez 0 pour désactiver le flou." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Rayon du flou en pixels derrière la carte de connexion en mode clair. " +"Utilisez 0 pour désactiver le flou." + +msgid "Cancel" +msgstr "Annuler" + +msgid "Card background" +msgstr "Arrière-plan de la carte" + +msgid "Card shadow" +msgstr "Ombre de la carte" + +msgid "Check for updates" +msgstr "Vérifier les mises à jour" + +msgid "Checking for updates..." +msgstr "Recherche des mises à jour..." + +msgid "Choose color" +msgstr "Choisir une couleur" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Sélectionner la direction du texte pour les pages d'authentification et de " +"connexion. Le mode automatique résout les codes de langue arabe et persan " +"(ar, ar_*, ar-*, fa, fa_*, fa-*) en RTL et revient à LTR pour les langues " +"manquantes ou non reconnues." + +msgid "Clean up" +msgstr "Nettoyer" + +msgid "Clean up invalid items" +msgstr "Supprimer les éléments invalides" + +msgid "Clean up invalid items (%d)" +msgstr "Supprimer les éléments invalides (%d)" + +msgid "Close" +msgstr "Fermer" + +msgid "Color mode" +msgstr "Mode de couleur" + +msgid "Colors" +msgstr "Couleurs" + +msgid "Comfortable (42px)" +msgstr "Confortable (42px)" + +msgid "Compact (32px)" +msgstr "Compact (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Configurez le mode de couleur, les couleurs d'accentuation, la taille de " +"l'interface, le comportement des animations, l'apparence de la page de " +"connexion et les surcharges CSS avancées pour luci-theme-fluent." + +msgid "Confirm" +msgstr "Confirmer" + +msgid "Confirm Installation" +msgstr "Confirmer l'installation" + +msgid "Continue" +msgstr "Continuer" + +msgid "Control height" +msgstr "Hauteur des commandes" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Contrôle la graisse de police utilisée par les étiquettes de navigation " +"principales et les éléments textuels du thème associés." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Contrôle les tokens Fluent communs pour le rayon des coins utilisés par les " +"cartes, les boutons, les champs de saisie et autres éléments d'interface " +"utilisateur." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Contrôle la durée des transitions Fluent partagées utilisées par les menus, " +"les en-têtes et autres animations du thème." + +msgid "Copied" +msgstr "Copié" + +msgid "Copy log" +msgstr "Copier le journal" + +msgid "Corner radius" +msgstr "Rayon des coins" + +msgid "Current / Pending MAC Address" +msgstr "Adresse MAC actuelle / En attente" + +msgid "Custom CSS" +msgstr "CSS personnalisé" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Personnalisez l'arrière-plan de la page de connexion, l'opacité des cartes " +"et le rayon de flou pour les modes clair et sombre." + +msgid "Dark mode" +msgstr "Mode sombre" + +msgid "Delete primary menu" +msgstr "Supprimer le menu principal" + +msgid "Disabled" +msgstr "Désactivé" + +msgid "Disconnects" +msgstr "Déconnexions" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Afficher l'indicateur de chargement thématique en haut pendant le chargement " +"des pages et les transitions." + +msgid "Download and Install" +msgstr "Télécharger et installer" + +msgid "Download log" +msgstr "Télécharger le journal" + +msgid "Downloaded" +msgstr "Téléchargement terminé" + +msgid "Downloading on router" +msgstr "Téléchargement sur le routeur" + +msgid "Drag to move second-level menu" +msgstr "Faire glisser pour déplacer un menu de second niveau" + +msgid "Drag to reorder primary menu" +msgstr "Faire glisser pour réorganiser les menus principaux" + +msgid "Drop second-level menus here" +msgstr "Déposer les menus de second niveau ici" + +msgid "Edit" +msgstr "Modifier" + +msgid "Enable page transition animation" +msgstr "Activer l'animation de transition de page" + +msgid "Enable tab underline animation" +msgstr "Activer l'animation du soulignement des onglets" + +msgid "Errors" +msgstr "Erreurs" + +msgid "Expecting: %s" +msgstr "Attendu : %s" + +msgid "Extra large (12px)" +msgstr "Très grand (12px)" + +msgid "Failed to check for updates" +msgstr "Échec de la vérification des mises à jour" + +msgid "Failed to fetch current theme version." +msgstr "Impossible de récupérer la version actuelle du thème." + +msgid "Fast" +msgstr "Rapide" + +msgid "Finished" +msgstr "Terminé" + +msgid "Fix primary menu title errors before saving." +msgstr "Corriger les erreurs dans le titre du menu principal avant d'enregistrer." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Thème Fluent Design 2 pour LuCI" + +msgid "Fluent Theme" +msgstr "Thème Fluent" + +msgid "Fluent theme settings" +msgstr "Paramètres du thème Fluent" + +msgid "Follow system" +msgstr "Suivre le système" + +msgid "Force dark mode" +msgstr "Forcer le mode sombre" + +msgid "Force light mode" +msgstr "Forcer le mode clair" + +msgid "Force LTR" +msgstr "Forcer l'orientation gauche à droite (LTR)" + +msgid "Force RTL" +msgstr "Forcer l'orientation droite à gauche (RTL)" + +msgid "General" +msgstr "Général" + +msgid "GitHub Token Required" +msgstr "Un jeton GitHub est requis" + +msgid "Header height" +msgstr "Hauteur de l'en-tête" + +msgid "Height of the top header bar in pixels." +msgstr "Hauteur de la barre d'en-tête supérieure en pixels." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Couleur HEX utilisée comme couleur d'accentuation principale Fluent lorsque " +"l'interface est affichée en mode sombre." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Couleur HEX utilisée comme couleur d'accentuation principale Fluent lorsque " +"l'interface est affichée en mode clair." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "Couleur HEX utilisée pour les éléments conteneur/carte en mode sombre." + +msgid "HEX color used for container/card elements in light mode." +msgstr "Couleur HEX utilisée pour les éléments conteneur/carte en mode clair." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"Couleur HEX utilisée pour les étiquettes des barres de progression lorsque " +"l'interface est affichée en mode sombre." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Couleur HEX utilisée pour les étiquettes des barres de progression lorsque " +"l'interface est affichée en mode clair." + +msgid "HEX color used for the main page background in dark mode." +msgstr "Couleur HEX utilisée pour le fond de la page principale en mode sombre." + +msgid "HEX color used for the main page background in light mode." +msgstr "Couleur HEX utilisée pour le fond de la page principale en mode clair." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "Couleur HEX utilisée pour la barre latérale de navigation en mode sombre." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "Couleur HEX utilisée pour la barre latérale de navigation en mode clair." + +msgid "Installation cancelled by user." +msgstr "Installation annulée par l'utilisateur." + +msgid "Installed version" +msgstr "Version installée" + +msgid "Installing package" +msgstr "Installation du paquet" + +msgid "Keep action bar visible while scrolling" +msgstr "Garder la barre d'actions visible pendant le défilement" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Afficher les boutons Enregistrer, Appliquer et Réinitialiser en bas de " +"l'écran pendant le défilement." + +msgid "Large" +msgstr "Grand" + +msgid "Large (8px)" +msgstr "Grand (8px)" + +msgid "Light mode" +msgstr "Mode clair" + +msgid "Line %d" +msgstr "Ligne %d" + +msgid "Lines" +msgstr "Lignes" + +msgid "Login" +msgstr "Connexion" + +msgid "Login card opacity" +msgstr "Opacité de la carte de connexion" + +msgid "Login page" +msgstr "Page de connexion" + +msgid "Medium" +msgstr "Moyen" + +msgid "Medium (4px)" +msgstr "Moyen (4px)" + +msgid "Menu" +msgstr "Menu" + +msgid "Menu title cannot be empty." +msgstr "Le titre du menu ne peut pas être vide." + +msgid "Navigation font weight" +msgstr "Poids de la police de navigation" + +msgid "New primary menu" +msgstr "Nouveau menu principal" + +msgid "New primary menu %d" +msgstr "Nouveau menu principal %d" + +msgid "Next" +msgstr "Suivant" + +msgid "Nightly build available (reinstallation check)." +msgstr "Build Nightly disponible (vérification de réinstallation)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Canal Nightly (pré-version)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" +"Aucun fichier du paquet correspondant à l'architecture de votre système n'a " +"été trouvé dans cette version." + +msgid "None" +msgstr "Aucun" + +msgid "Normal" +msgstr "Normal" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Opacité de la carte de connexion en mode sombre. 0 est totalement " +"transparent et 1 est totalement opaque." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Opacité de la carte de connexion en mode clair. 0 est totalement transparent " +"et 1 est totalement opaque." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"CSS brut optionnel injecté dans le modèle d'en-tête Fluent. Utilisez ceci " +"pour des remplacements supplémentaires de variables --fluent-* ou des " +"ajustements spécifiques à une page qui ne sont pas exposés en tant " +"qu'options dédiées." + +msgid "Package format" +msgstr "Format du paquet" + +msgid "Page background" +msgstr "Arrière-plan de la page" + +msgid "Please enter your password." +msgstr "Veuillez entrer votre mot de passe." + +msgid "Please enter your username." +msgstr "Veuillez entrer votre nom d'utilisateur." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Position des étiquettes de la barre de progression par rapport à la barre. " +"L'alignement du début/de la fin suit la direction du texte (LTR ou RTL)." + +msgid "Preset Vendor / Generator" +msgstr "Fournisseur / Générateur par défaut" + +msgid "Primary menu title" +msgstr "Titre du menu principal" + +msgid "Primary menu titles cannot be empty." +msgstr "Les titres du menu principal ne peuvent pas être vides." + +msgid "Primary menu titles must be unique." +msgstr "Les titres du menu principal doivent être uniques." + +msgid "Progress bar text color" +msgstr "Couleur du texte de la barre de progression" + +msgid "Progress bar text position" +msgstr "Position du texte de la barre de progression" + +msgid "Random Local MAC" +msgstr "MAC local aléatoire" + +msgid "Randomize MAC" +msgstr "Aléatoire de l'adresse MAC" + +msgid "Reload Web Interface" +msgstr "Recharger l'interface web" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" +"Remplacer les éléments de sélection natifs par le widget déroulant " +"personnalisé du thème." + +msgid "Respect reduced-motion preference" +msgstr "Respecter la préférence de réduction des animations." + +msgid "Restore %s to its original menu name" +msgstr "Restaurer le nom original de %s" + +msgid "Restore %s to its original menu position" +msgstr "Restaurer la position originale de %s" + +msgid "Restore defaults" +msgstr "Restaurer les paramètres par défaut" + +msgid "Restore to original title" +msgstr "Restaurer le titre original" + +msgid "Retrying with token..." +msgstr "Tentative avec le token..." + +msgid "Rounded (6px)" +msgstr "Arrondi (6px)" + +msgid "Save" +msgstr "Enregistrer" + +msgid "Scroll to bottom" +msgstr "Aller à la fin" + +msgid "Scroll to top" +msgstr "Retour en haut" + +msgid "Search menu items" +msgstr "Rechercher dans les éléments du menu" + +msgid "Search menu..." +msgstr "Rechercher dans le menu..." + +msgid "Second-level menu title" +msgstr "Titre du sous-menu" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "" +"Sélectionner la profondeur de l'ombre appliquée aux cartes et panneaux " +"thématiques." + +msgid "Select vendor prefix..." +msgstr "Sélectionner le préfixe du fournisseur..." + +msgid "Semibold" +msgstr "Semi-gras" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Définit la taille de base de la police de l'interface en pixels. La plupart " +"des textes du thème sont mis à l'échelle à partir de cette valeur via les " +"variables CSS Fluent. Plage recommandée : 12-18px." + +msgid "Show %s in the menu" +msgstr "Afficher %s dans le menu" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Afficher les modifications UCI en attente dans le message d'état principal " +"jusqu'à la fin de l'application de la configuration." + +msgid "Show top loading bar" +msgstr "Afficher la barre de chargement supérieure" + +msgid "Show UCI changes while applying configuration" +msgstr "Afficher les modifications UCI pendant l'application de la configuration." + +msgid "Sidebar background" +msgstr "Arrière-plan de la barre latérale" + +msgid "Sidebar width" +msgstr "Largeur de la barre latérale" + +msgid "Slow" +msgstr "Lent" + +msgid "Small" +msgstr "Petit" + +msgid "Small (2px)" +msgstr "Petit (2px)" + +msgid "Software Update" +msgstr "Mise à jour du logiciel" + +msgid "Source code" +msgstr "Code source" + +msgid "Square (0px)" +msgstr "Carré (0px)" + +msgid "Stable Channel" +msgstr "Canal stable" + +msgid "Starting backend download..." +msgstr "Début du téléchargement de l'arrière-plan..." + +msgid "Starting update process..." +msgstr "Début du processus de mise à jour..." + +msgid "Submit" +msgstr "Envoyer" + +msgid "Switch theme mode" +msgstr "Changer le mode du thème" + +msgid "Text direction" +msgstr "Direction du texte" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"La configuration du menu enregistée est invalide. Choisissez \"Restaurer les " +"paramètres par défaut\" ou effectuez une modification avant d'enregistrer." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"La configuration du menu enregistée est invalide. Restaurez les paramètres " +"par défaut ou modifiez la configuration avant d'enregistrer." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Le thème est en cours d'installation sur votre routeur. Les journaux sont " +"affichés ci-dessous. Le rechargement de l'interface web appliquera les " +"modifications une fois que RPCD redémarrera." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "" +"Le paquet du thème est prêt. Êtes-vous sûr de vouloir procéder à " +"l'installation ?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Ce jeton ne nécessite aucune autorisation/portée particulière (un accès " +"public en lecture seule est suffisant)." + +msgid "Theme settings" +msgstr "Paramètres du thème" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Thème mis à jour avec succès ! Rechargement du service RPC, veuillez " +"actualiser la page dans 5 secondes." + +msgid "Theme transition speed" +msgstr "Vitesse de transition du thème" + +msgid "Toggle fullscreen" +msgstr "Basculer en plein écran" + +msgid "Triggering installation on router..." +msgstr "Installation en cours sur le routeur..." + +msgid "Update channel" +msgstr "Canal de mise à jour" + +msgid "Update failed" +msgstr "Mise à jour échouée" + +msgid "Upgrade Successful!" +msgstr "Mise à niveau réussie !" + +msgid "Use Fluent custom select dropdowns" +msgstr "Utiliser les menus déroulants personnalisés Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Utiliser l'API View Transition du navigateur pour animer la navigation entre " +"les pages LuCI, si prise en charge." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Utiliser la préférence système/navigateur, ou afficher toujours le thème " +"Fluent avec une palette claire ou sombre fixe." + +msgid "Username is required." +msgstr "Le nom d'utilisateur est requis." + +msgid "valid HEX color value" +msgstr "valeur de couleur HEX valide" + +msgid "Warnings" +msgstr "Avertissements" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Si activé, les animations Fluent respectent la préférence de réduction des " +"mouvements du navigateur ou du système d'exploitation." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Largeur de la barre latérale de navigation principale en pixels." + +msgid "Word wrap" +msgstr "Retour à la ligne" + +msgid "You can create a new token at" +msgstr "Vous pouvez créer un nouveau jeton sur :" + +msgid "Your theme is up to date!" +msgstr "Votre thème est à jour !" diff --git a/luci-theme-fluent/po/it/fluent.po b/luci-theme-fluent/po/it/fluent.po new file mode 100644 index 000000000..06d48866c --- /dev/null +++ b/luci-theme-fluent/po/it/fluent.po @@ -0,0 +1,790 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: it\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" + +msgid "%d second-level menus" +msgstr "%d sottomenu" + +msgid "A new version is available!" +msgstr "È disponibile una nuova versione!" + +msgid "About" +msgstr "Informazioni" + +msgid "Above bar, end" +msgstr "Fine barra, estremità" + +msgid "Above bar, start" +msgstr "Inizio barra" + +msgid "Accent color" +msgstr "Colore accento" + +msgid "Add primary menu" +msgstr "Aggiungi menu principale" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Regola il layout, la tipografia, i tempi di transizione, le ombre e aggiungi " +"variabili CSS personalizzate o regole quando i controlli integrati non sono " +"sufficienti." + +msgid "Advanced" +msgstr "Avanzate" + +msgid "Advanced MAC Selector" +msgstr "Selettore MAC avanzato" + +msgid "and %d more..." +msgstr "e %d altri..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Anima la sottolineatura attiva quando si passa tra le schede native di LuCI " +"e i menu a schede tematici." + +msgid "Animation" +msgstr "Animazione" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Limite di richieste API superato. Inserisci un token GitHub per continuare. " +"Questo token viene utilizzato solo per le richieste del frontend e non verrà " +"salvato sul backend del router." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Si applica a pulsanti, campi di input, menu a tendina e controlli modulo " +"simili in tutto il tema." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Sei sicuro di voler eliminare %d voci o regole del menu non valide/non " +"installate?" + +msgid "Author" +msgstr "Autore" + +msgid "Auto-scroll" +msgstr "Scorrimento automatico" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Automatico (solo per le impostazioni locali arabo/persiano)" + +msgid "Backdrop blur radius" +msgstr "Raggio di sfocatura dello sfondo" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Backend (Accelerazione GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "Backend (GitHub ufficiale)" + +msgid "Base font size" +msgstr "Dimensione base del font" + +msgid "Below bar, end" +msgstr "Fine barra" + +msgid "Below bar, start" +msgstr "Inizio barra" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Raggio di sfocatura in pixel dietro la scheda di accesso in modalità scura. " +"Usare 0 per disabilitare la sfocatura." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Raggio di sfocatura in pixel dietro la scheda di accesso in modalità chiara. " +"Usare 0 per disabilitare la sfocatura." + +msgid "Cancel" +msgstr "Annulla" + +msgid "Card background" +msgstr "Sfondo della scheda" + +msgid "Card shadow" +msgstr "Ombra della scheda" + +msgid "Check for updates" +msgstr "Verifica aggiornamenti" + +msgid "Checking for updates..." +msgstr "Controllo aggiornamenti..." + +msgid "Choose color" +msgstr "Scegli colore" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Seleziona la direzione del testo per le pagine di autenticazione e accesso. " +"La modalità automatica risolve i codici lingua araba e persiana (ar, ar_*, " +"ar-*, fa, fa_*, fa-*) in RTL e ricade su LTR per lingue mancanti o non " +"riconosciute." + +msgid "Clean up" +msgstr "Pulisci" + +msgid "Clean up invalid items" +msgstr "Elimina elementi non validi" + +msgid "Clean up invalid items (%d)" +msgstr "Elimina elementi non validi (%d)" + +msgid "Close" +msgstr "Chiudi" + +msgid "Color mode" +msgstr "Modalità colore" + +msgid "Colors" +msgstr "Colori" + +msgid "Comfortable (42px)" +msgstr "Comodo (42px)" + +msgid "Compact (32px)" +msgstr "Compatto (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Configura la modalità colore, i colori di accento, le dimensioni del layout, " +"il comportamento delle animazioni, l'aspetto della pagina di accesso e le " +"personalizzazioni CSS avanzate per luci-theme-fluent." + +msgid "Confirm" +msgstr "Conferma" + +msgid "Confirm Installation" +msgstr "Conferma installazione" + +msgid "Continue" +msgstr "Continua" + +msgid "Control height" +msgstr "Altezza dei controlli" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Controlla lo spessore del carattere utilizzato per le etichette della " +"navigazione principale e i testi di accento del tema." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Controlla il raggio d'angolo Fluent condiviso, utilizzato da schede, " +"pulsanti, campi di input e altre superfici dell'interfaccia utente." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Controlla la temporizzazione delle transizioni Fluent condivisa, utilizzata " +"per i menu, le intestazioni e altre animazioni del tema." + +msgid "Copied" +msgstr "Copiato" + +msgid "Copy log" +msgstr "Copia registro" + +msgid "Corner radius" +msgstr "Raggio d'angolo" + +msgid "Current / Pending MAC Address" +msgstr "Indirizzo MAC corrente / in attesa" + +msgid "Custom CSS" +msgstr "CSS personalizzato" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Personalizza lo sfondo della pagina di accesso, l'opacità delle schede e il " +"raggio di sfocatura per le modalità chiara e scura." + +msgid "Dark mode" +msgstr "Modalità scura" + +msgid "Delete primary menu" +msgstr "Elimina menu principale" + +msgid "Disabled" +msgstr "Disabilitato" + +msgid "Disconnects" +msgstr "Disconnessioni" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Mostra l'indicatore di caricamento a tema sul bordo superiore durante il " +"caricamento delle pagine e le transizioni." + +msgid "Download and Install" +msgstr "Scarica e Installa" + +msgid "Download log" +msgstr "Scarica registro" + +msgid "Downloaded" +msgstr "Scaricato" + +msgid "Downloading on router" +msgstr "Scaricamento in corso sul router" + +msgid "Drag to move second-level menu" +msgstr "Trascina per spostare il sottomenu" + +msgid "Drag to reorder primary menu" +msgstr "Trascina per riordinare i menu principali" + +msgid "Drop second-level menus here" +msgstr "Rilascia i sottomenu qui" + +msgid "Edit" +msgstr "Modifica" + +msgid "Enable page transition animation" +msgstr "Abilita l'animazione di transizione della pagina" + +msgid "Enable tab underline animation" +msgstr "Abilita l'animazione della sottolineatura delle schede" + +msgid "Errors" +msgstr "Errori" + +msgid "Expecting: %s" +msgstr "Atteso: %s" + +msgid "Extra large (12px)" +msgstr "Molto grande (12px)" + +msgid "Failed to check for updates" +msgstr "Impossibile verificare la presenza di aggiornamenti" + +msgid "Failed to fetch current theme version." +msgstr "Impossibile recuperare la versione corrente del tema." + +msgid "Fast" +msgstr "Veloce" + +msgid "Finished" +msgstr "Completato" + +msgid "Fix primary menu title errors before saving." +msgstr "Correggere gli errori nei titoli dei menu principali prima di salvare." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Tema Fluent Design 2 per LuCI" + +msgid "Fluent Theme" +msgstr "Tema Fluent" + +msgid "Fluent theme settings" +msgstr "Impostazioni del tema Fluent" + +msgid "Follow system" +msgstr "Segui il sistema" + +msgid "Force dark mode" +msgstr "Forza la modalità scura" + +msgid "Force light mode" +msgstr "Forza la modalità chiara" + +msgid "Force LTR" +msgstr "Forza l'orientamento da sinistra a destra (LTR)" + +msgid "Force RTL" +msgstr "Forza l'orientamento da destra a sinistra (RTL)" + +msgid "General" +msgstr "Generale" + +msgid "GitHub Token Required" +msgstr "Richiede un token GitHub" + +msgid "Header height" +msgstr "Altezza dell'intestazione" + +msgid "Height of the top header bar in pixels." +msgstr "Altezza della barra di intestazione superiore in pixel." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Colore esadecimale utilizzato come colore accento principale di Fluent " +"quando l'interfaccia è visualizzata in modalità scura." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Colore esadecimale utilizzato come colore accento principale di Fluent " +"quando l'interfaccia è visualizzata in modalità chiara." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "" +"Colore esadecimale utilizzato per gli elementi contenitore/scheda in " +"modalità scura." + +msgid "HEX color used for container/card elements in light mode." +msgstr "" +"Colore esadecimale utilizzato per gli elementi contenitore/scheda in " +"modalità chiara." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"Colore esadecimale utilizzato per le etichette delle barre di avanzamento " +"quando l'interfaccia è visualizzata in modalità scura." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Colore esadecimale utilizzato per le etichette delle barre di avanzamento " +"quando l'interfaccia è visualizzata in modalità chiara." + +msgid "HEX color used for the main page background in dark mode." +msgstr "" +"Colore esadecimale utilizzato per lo sfondo della pagina principale in " +"modalità scura." + +msgid "HEX color used for the main page background in light mode." +msgstr "" +"Colore esadecimale utilizzato per lo sfondo della pagina principale in " +"modalità chiara." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "" +"Colore esadecimale utilizzato per la barra laterale di navigazione in " +"modalità scura." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "" +"Colore esadecimale utilizzato per la barra laterale di navigazione in " +"modalità chiara." + +msgid "Installation cancelled by user." +msgstr "Installazione annullata dall'utente." + +msgid "Installed version" +msgstr "Versione installata" + +msgid "Installing package" +msgstr "Installazione del pacchetto" + +msgid "Keep action bar visible while scrolling" +msgstr "Mantieni la barra degli strumenti visibile durante lo scorrimento." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Mantieni i pulsanti Salva, Applica e Ripristina fissi in basso durante lo " +"scorrimento." + +msgid "Large" +msgstr "Grande" + +msgid "Large (8px)" +msgstr "Grande (8px)" + +msgid "Light mode" +msgstr "Modalità chiara" + +msgid "Line %d" +msgstr "Riga %d" + +msgid "Lines" +msgstr "Righe" + +msgid "Login" +msgstr "Accesso" + +msgid "Login card opacity" +msgstr "Opacità della scheda di accesso" + +msgid "Login page" +msgstr "Pagina di accesso" + +msgid "Medium" +msgstr "Media" + +msgid "Medium (4px)" +msgstr "Media (4px)" + +msgid "Menu" +msgstr "Menu" + +msgid "Menu title cannot be empty." +msgstr "Il titolo del menu non può essere vuoto." + +msgid "Navigation font weight" +msgstr "Peso del carattere della navigazione" + +msgid "New primary menu" +msgstr "Nuovo menu principale" + +msgid "New primary menu %d" +msgstr "Nuovo menu principale %d" + +msgid "Next" +msgstr "Avanti" + +msgid "Nightly build available (reinstallation check)." +msgstr "Build Nightly disponibile (controllo reinstallazione)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Canale Nightly (Anteprima)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "Nessun pacchetto disponibile per la tua architettura in questa versione." + +msgid "None" +msgstr "Nessuno" + +msgid "Normal" +msgstr "Normale" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Opacità della scheda di accesso in modalità scura. 0 è completamente " +"trasparente e 1 è completamente opaco." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Opacità della scheda di accesso in modalità chiara. 0 è completamente " +"trasparente e 1 è completamente opaco." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"CSS grezzo facoltativo iniettato nel template dell'header Fluent. Usare per " +"sovrascrivere variabili --fluent-* aggiuntive o modifiche specifiche della " +"pagina che non sono esposte come opzioni dedicate." + +msgid "Package format" +msgstr "Formato pacchetto" + +msgid "Page background" +msgstr "Sfondo pagina" + +msgid "Please enter your password." +msgstr "Inserisci la tua password." + +msgid "Please enter your username." +msgstr "Inserisci il tuo nome utente." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Posizione delle etichette della barra di avanzamento rispetto alla barra. " +"L'allineamento inizio/fine segue la direzione del testo (da sinistra a " +"destra o da destra a sinistra)." + +msgid "Preset Vendor / Generator" +msgstr "Produttore / Generatore predefinito" + +msgid "Primary menu title" +msgstr "Titolo del menu principale" + +msgid "Primary menu titles cannot be empty." +msgstr "I titoli del menu principale non possono essere vuoti." + +msgid "Primary menu titles must be unique." +msgstr "I titoli del menu principale devono essere univoci." + +msgid "Progress bar text color" +msgstr "Colore del testo della barra di avanzamento" + +msgid "Progress bar text position" +msgstr "Posizione del testo nella barra di avanzamento" + +msgid "Random Local MAC" +msgstr "MAC locale casuale" + +msgid "Randomize MAC" +msgstr "Randomizza MAC" + +msgid "Reload Web Interface" +msgstr "Ricarica interfaccia web" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" +"Sostituisci gli elementi select nativi con il widget dropdown personalizzato " +"del tema." + +msgid "Respect reduced-motion preference" +msgstr "Rispetta la preferenza per ridotta animazione." + +msgid "Restore %s to its original menu name" +msgstr "Ripristina %s al nome del menu originale" + +msgid "Restore %s to its original menu position" +msgstr "Ripristina %s alla posizione originale nel menu" + +msgid "Restore defaults" +msgstr "Ripristina le impostazioni predefinite" + +msgid "Restore to original title" +msgstr "Ripristina il titolo originale" + +msgid "Retrying with token..." +msgstr "Riprovo con token..." + +msgid "Rounded (6px)" +msgstr "Arrotondato (6px)" + +msgid "Save" +msgstr "Salva" + +msgid "Scroll to bottom" +msgstr "Scorri in fondo" + +msgid "Scroll to top" +msgstr "Torna in alto" + +msgid "Search menu items" +msgstr "Cerca elementi del menu" + +msgid "Search menu..." +msgstr "Cerca nel menu..." + +msgid "Second-level menu title" +msgstr "Titolo del sottomenu" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "" +"Seleziona la profondità dell'ombra applicata alle schede e ai pannelli " +"tematici." + +msgid "Select vendor prefix..." +msgstr "Seleziona il prefisso del fornitore..." + +msgid "Semibold" +msgstr "Semi-grassetto" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Imposta la dimensione base del font dell'interfaccia in pixel. La maggior " +"parte del testo del tema scala da questo valore tramite le variabili CSS di " +"Fluent. Intervallo consigliato: 12-18px." + +msgid "Show %s in the menu" +msgstr "Mostra %s nel menu" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Mostra le modifiche UCI in sospeso nel messaggio di stato superiore finché " +"l'applicazione della configurazione non è completata." + +msgid "Show top loading bar" +msgstr "Mostra la barra di caricamento superiore" + +msgid "Show UCI changes while applying configuration" +msgstr "Mostra le modifiche UCI durante l'applicazione della configurazione." + +msgid "Sidebar background" +msgstr "Sfondo della barra laterale" + +msgid "Sidebar width" +msgstr "Larghezza della barra laterale" + +msgid "Slow" +msgstr "Lento" + +msgid "Small" +msgstr "Piccolo" + +msgid "Small (2px)" +msgstr "Piccolo (2px)" + +msgid "Software Update" +msgstr "Aggiornamento software" + +msgid "Source code" +msgstr "Codice sorgente" + +msgid "Square (0px)" +msgstr "Quadrato (0px)" + +msgid "Stable Channel" +msgstr "Canale stabile" + +msgid "Starting backend download..." +msgstr "Avvio del download dal server..." + +msgid "Starting update process..." +msgstr "Avvio del processo di aggiornamento..." + +msgid "Submit" +msgstr "Invia" + +msgid "Switch theme mode" +msgstr "Cambia modalità tema" + +msgid "Text direction" +msgstr "Direzione testo" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"La disposizione del menu salvata non è valida. Scegli \"Ripristina le " +"impostazioni predefinite\" o modifica prima di salvare." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"La disposizione del menu salvata non è valida. Ripristina le impostazioni " +"predefinite o modifica la disposizione prima di salvare." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Il tema è in fase di installazione sul router. I log sono visualizzati qui " +"sotto. Il caricamento della web interface applicherà le modifiche una volta " +"riavviato RPCD." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "" +"Il pacchetto del tema è pronto. Sei sicuro di voler procedere con " +"l'installazione?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Il token NON richiede alcuna autorizzazione/ambito (l'accesso pubblico in " +"sola lettura è sufficiente)." + +msgid "Theme settings" +msgstr "Impostazioni del tema" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Tema aggiornato con successo! Ricaricamento del servizio RPC, si prega di " +"aggiornare la pagina tra 5 secondi." + +msgid "Theme transition speed" +msgstr "Velocità di transizione del tema" + +msgid "Toggle fullscreen" +msgstr "Attiva/disattiva schermo intero" + +msgid "Triggering installation on router..." +msgstr "Avvio dell'installazione sul router..." + +msgid "Update channel" +msgstr "Canale di aggiornamento" + +msgid "Update failed" +msgstr "Aggiornamento fallito" + +msgid "Upgrade Successful!" +msgstr "Aggiornamento completato!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Utilizza i menu a tendina personalizzati di Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Utilizza la View Transition API del browser per animare la navigazione tra " +"le pagine di LuCI, quando supportata." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Utilizza le preferenze del sistema/browser, oppure visualizza sempre il tema " +"Fluent con una palette fissa (chiara o scura)." + +msgid "Username is required." +msgstr "È necessario specificare un nome utente." + +msgid "valid HEX color value" +msgstr "valore esadecimale di colore valido" + +msgid "Warnings" +msgstr "Avvisi" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Se abilitato, le animazioni di Fluent seguono le preferenze del browser o " +"del sistema operativo relative alla riduzione dei movimenti." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Larghezza della barra laterale principale di navigazione in pixel." + +msgid "Word wrap" +msgstr "Ritorno a capo" + +msgid "You can create a new token at" +msgstr "È possibile creare un nuovo token su:" + +msgid "Your theme is up to date!" +msgstr "Il tuo tema è aggiornato." diff --git a/luci-theme-fluent/po/ja/fluent.po b/luci-theme-fluent/po/ja/fluent.po new file mode 100644 index 000000000..b8303a3d4 --- /dev/null +++ b/luci-theme-fluent/po/ja/fluent.po @@ -0,0 +1,707 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: ja\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" + +msgid "%d second-level menus" +msgstr "%d 個のセカンドレベルメニュー" + +msgid "A new version is available!" +msgstr "新しいバージョンが利用可能です!" + +msgid "About" +msgstr "情報" + +msgid "Above bar, end" +msgstr "バーの上、終了" + +msgid "Above bar, start" +msgstr "バーの上、開始" + +msgid "Accent color" +msgstr "アクセントカラー" + +msgid "Add primary menu" +msgstr "プライマリメニューを追加" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "レイアウト、タイポグラフィ、トランジションのタイミング、影を調整し、組み込みコントロールが不十分な場合に、カスタムCSS変数やルールを挿入できます。" + +msgid "Advanced" +msgstr "詳細設定" + +msgid "Advanced MAC Selector" +msgstr "高度なMACアドレス選択" + +msgid "and %d more..." +msgstr "および%d個以上..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "LuCIの標準タブとテーマ付きタブメニュー間を切り替える際に、アクティブなアンダーラインのアニメーションを表示します。" + +msgid "Animation" +msgstr "アニメーション" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"APIレート制限を超えました。続行するには、GitHubトークンを入力してください。このトークンはフロントエンドのリクエストのみに使用され、ルーターのバ" +"ックエンドには保存されません。" + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "テーマ全体で、標準ボタン、入力フィールド、選択肢などのフォームコントロールに適用されます。" + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "%d個の無効または未インストールのメニュー項目やルールを削除しますか?" + +msgid "Author" +msgstr "作者" + +msgid "Auto-scroll" +msgstr "自動スクロール" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "自動 (アラビア語/ペルシャ語ロケールのみ)" + +msgid "Backdrop blur radius" +msgstr "背景のぼかし半径" + +msgid "Backend (GHProxy Acceleration)" +msgstr "バックエンド (GHProxyによる高速化)" + +msgid "Backend (Official GitHub)" +msgstr "バックエンド (公式GitHub)" + +msgid "Base font size" +msgstr "ベースフォントサイズ" + +msgid "Below bar, end" +msgstr "バーの下、終了" + +msgid "Below bar, start" +msgstr "バーの下、開始" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "ダークモード時のログインカード背面のぼかし半径(ピクセル)。ぼかしを無効にするには0を使用してください。" + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "ライトモード時のログインカード背面のぼかし半径(ピクセル)。ぼかしを無効にするには0を使用してください。" + +msgid "Cancel" +msgstr "キャンセル" + +msgid "Card background" +msgstr "カード背景色" + +msgid "Card shadow" +msgstr "カードの影" + +msgid "Check for updates" +msgstr "アップデートを確認" + +msgid "Checking for updates..." +msgstr "アップデートをチェック中..." + +msgid "Choose color" +msgstr "色を選択" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"認証ページとログインページのドキュメント方向を選択します。自動モードでは、アラビア語およびペルシャ語のロケールコード (ar, ar_*, ar-*, " +"fa, fa_*, fa-*) が RTL に解決され、不明または認識されないロケールの場合は LTR にフォールバックされます。" + +msgid "Clean up" +msgstr "クリーンアップ" + +msgid "Clean up invalid items" +msgstr "無効な項目を削除" + +msgid "Clean up invalid items (%d)" +msgstr "無効な項目を削除 (%d)" + +msgid "Close" +msgstr "閉じる" + +msgid "Color mode" +msgstr "カラーモード" + +msgid "Colors" +msgstr "色" + +msgid "Comfortable (42px)" +msgstr "快適 (42px)" + +msgid "Compact (32px)" +msgstr "コンパクト (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"カラーモード、アクセントカラー、レイアウトサイズ、アニメーション設定、ログインページの外観、および luci-theme-fluent の高度な CSS " +"上書きを設定します。" + +msgid "Confirm" +msgstr "確認" + +msgid "Confirm Installation" +msgstr "インストールを確認" + +msgid "Continue" +msgstr "続行" + +msgid "Control height" +msgstr "コントロールの高さ" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "メインナビゲーションラベルおよび関連するテーマテキストの装飾に使用されるフォントウェイトを制御します。" + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "カード、ボタン、入力フィールド、および関連する UI 要素で使用される共通 Fluent コーナー半径値を制御します。" + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "メニュー、ヘッダー、その他のテーマアニメーションで使用される共通 Fluent トランジションタイミングを制御します。" + +msgid "Copied" +msgstr "コピー済み" + +msgid "Copy log" +msgstr "ログをコピー" + +msgid "Corner radius" +msgstr "コーナー半径" + +msgid "Current / Pending MAC Address" +msgstr "現在の / 待機中の MAC アドレス" + +msgid "Custom CSS" +msgstr "カスタム CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "ライトモードとダークモードのログインページ背景、カードの不透明度、およびぼかし半径をカスタマイズします。" + +msgid "Dark mode" +msgstr "ダークモード" + +msgid "Delete primary menu" +msgstr "プライマリメニューを削除" + +msgid "Disabled" +msgstr "無効" + +msgid "Disconnects" +msgstr "切断回数" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "ページ読み込みおよび遷移中に、テーマ適用されたローディングインジケータを画面上部に表示します。" + +msgid "Download and Install" +msgstr "ダウンロードとインストール" + +msgid "Download log" +msgstr "ログのダウンロード" + +msgid "Downloaded" +msgstr "ダウンロード完了" + +msgid "Downloading on router" +msgstr "ルーターでのダウンロード中" + +msgid "Drag to move second-level menu" +msgstr "セカンドレベルメニューをドラッグして移動" + +msgid "Drag to reorder primary menu" +msgstr "プライマリメニューの順序を入れ替えるためにドラッグ" + +msgid "Drop second-level menus here" +msgstr "ここにセカンドレベルメニューをドロップ" + +msgid "Edit" +msgstr "編集" + +msgid "Enable page transition animation" +msgstr "ページ遷移アニメーションを有効にする" + +msgid "Enable tab underline animation" +msgstr "タブのアンダーラインアニメーションを有効にする" + +msgid "Errors" +msgstr "エラー" + +msgid "Expecting: %s" +msgstr "期待値: %s" + +msgid "Extra large (12px)" +msgstr "非常に大きい (12px)" + +msgid "Failed to check for updates" +msgstr "アップデートの確認に失敗しました" + +msgid "Failed to fetch current theme version." +msgstr "現在のテーマバージョンの取得に失敗しました。" + +msgid "Fast" +msgstr "高速" + +msgid "Finished" +msgstr "完了" + +msgid "Fix primary menu title errors before saving." +msgstr "保存する前に、プライマリメニューのタイトルエラーを修正してください。" + +msgid "Fluent Design 2 theme for LuCI" +msgstr "LuCI用のFluent Design 2テーマ" + +msgid "Fluent Theme" +msgstr "Fluentテーマ" + +msgid "Fluent theme settings" +msgstr "Fluentテーマの設定" + +msgid "Follow system" +msgstr "システムに合わせる" + +msgid "Force dark mode" +msgstr "ダークモードを強制" + +msgid "Force light mode" +msgstr "ライトモードを強制" + +msgid "Force LTR" +msgstr "左から右 (LTR) を強制" + +msgid "Force RTL" +msgstr "右から左 (RTL) を強制" + +msgid "General" +msgstr "基本設定" + +msgid "GitHub Token Required" +msgstr "GitHub トークンが必要です" + +msgid "Header height" +msgstr "ヘッダーの高さ" + +msgid "Height of the top header bar in pixels." +msgstr "上部のヘッダーバーの高さをピクセル単位で指定します。" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "インターフェースがダークモードで表示される際の、主要な Fluent アクセントカラー (HEX 形式)。" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "インターフェースがライトモードで表示される際の、主要な Fluent アクセントカラー (HEX 形式)。" + +msgid "HEX color used for container/card elements in dark mode." +msgstr "ダークモードでコンテナ/カード要素に使用する HEX カラー。" + +msgid "HEX color used for container/card elements in light mode." +msgstr "ライトモードでコンテナ/カード要素に使用する HEX カラー。" + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "ダークモードでインターフェースが表示される際の、プログレスバーのラベルに使用する HEX カラー。" + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "ライトモードでインターフェースが表示される際の、プログレスバーのラベルに使用する HEX カラー。" + +msgid "HEX color used for the main page background in dark mode." +msgstr "ダークモードでメインページの背景に使用する HEX カラー。" + +msgid "HEX color used for the main page background in light mode." +msgstr "ライトモードでメインページの背景に使用する HEX カラー。" + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "ダークモード時のナビゲーションサイドバーで使用するHEXカラー。" + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "ライトモード時のナビゲーションサイドバーで使用するHEXカラー。" + +msgid "Installation cancelled by user." +msgstr "ユーザーによるインストールの中止。" + +msgid "Installed version" +msgstr "インストール済みバージョン" + +msgid "Installing package" +msgstr "パッケージのインストール中" + +msgid "Keep action bar visible while scrolling" +msgstr "スクロール中にアクションバーを表示する" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "保存、適用、およびリセットのアクションを、スクロール中に画面下部に固定表示します。" + +msgid "Large" +msgstr "大" + +msgid "Large (8px)" +msgstr "大 (8px)" + +msgid "Light mode" +msgstr "ライトモード" + +msgid "Line %d" +msgstr "%d 行目" + +msgid "Lines" +msgstr "行数" + +msgid "Login" +msgstr "ログイン" + +msgid "Login card opacity" +msgstr "ログインカードの不透明度" + +msgid "Login page" +msgstr "ログインページ" + +msgid "Medium" +msgstr "中" + +msgid "Medium (4px)" +msgstr "中 (4px)" + +msgid "Menu" +msgstr "メニュー" + +msgid "Menu title cannot be empty." +msgstr "メニューのタイトルは空にできません。" + +msgid "Navigation font weight" +msgstr "ナビゲーションフォントの太さ" + +msgid "New primary menu" +msgstr "新しいプライマリメニュー" + +msgid "New primary menu %d" +msgstr "新しいプライマリメニュー %d" + +msgid "Next" +msgstr "次へ" + +msgid "Nightly build available (reinstallation check)." +msgstr "Nightly ビルドが利用可能です (再インストール確認)。" + +msgid "Nightly Channel (Prerelease)" +msgstr "Nightly チャンネル (プレリリース)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "このリリースで、お客様のシステムアーキテクチャに一致するパッケージが見つかりませんでした。" + +msgid "None" +msgstr "なし" + +msgid "Normal" +msgstr "通常" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "ダークモード時のログインカードの不透明度。0は完全に透明、1は完全に不透明です。" + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "ライトモード時のログインカードの不透明度。0は完全に透明、1は完全に不透明です。" + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Fluentヘッダーテンプレートに挿入されるオプションの生のCSS。追加の--fluent-*変数のオーバーライドや、専用のオプションとして公開されてい" +"ないページ固有の調整に使用します。" + +msgid "Package format" +msgstr "パッケージ形式" + +msgid "Page background" +msgstr "ページの背景" + +msgid "Please enter your password." +msgstr "パスワードを入力してください。" + +msgid "Please enter your username." +msgstr "ユーザー名を入力してください。" + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "プログレスバーのラベルの位置は、バーに対して相対的に設定されます。開始/終了位置は、テキスト方向(LTRまたはRTL)に従います。" + +msgid "Preset Vendor / Generator" +msgstr "プリセットのベンダー/ジェネレーター" + +msgid "Primary menu title" +msgstr "メインメニューのタイトル" + +msgid "Primary menu titles cannot be empty." +msgstr "メインメニューのタイトルは空にできません。" + +msgid "Primary menu titles must be unique." +msgstr "メインメニューのタイトルは一意である必要があります。" + +msgid "Progress bar text color" +msgstr "プログレスバーのテキストの色" + +msgid "Progress bar text position" +msgstr "プログレスバーのテキスト位置" + +msgid "Random Local MAC" +msgstr "ランダムなローカルMACアドレス" + +msgid "Randomize MAC" +msgstr "MACアドレスをランダム化" + +msgid "Reload Web Interface" +msgstr "Webインターフェースを再読み込み" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "ネイティブの選択要素を、テーマ独自のドロップダウンウィジェットに置き換えます。" + +msgid "Respect reduced-motion preference" +msgstr "アニメーションの抑制設定を尊重します" + +msgid "Restore %s to its original menu name" +msgstr "%s を元のメニュー名に戻す" + +msgid "Restore %s to its original menu position" +msgstr "%s を元の位置に戻す" + +msgid "Restore defaults" +msgstr "デフォルトを復元" + +msgid "Restore to original title" +msgstr "元のタイトルに戻す" + +msgid "Retrying with token..." +msgstr "トークンを使って再試行中..." + +msgid "Rounded (6px)" +msgstr "角丸 (6px)" + +msgid "Save" +msgstr "保存" + +msgid "Scroll to bottom" +msgstr "下部にスクロール" + +msgid "Scroll to top" +msgstr "上部へスクロール" + +msgid "Search menu items" +msgstr "メニュー項目を検索" + +msgid "Search menu..." +msgstr "メニューを検索..." + +msgid "Second-level menu title" +msgstr "セカンドレベルメニューのタイトル" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "テーマ適用カードやパネルに適用する影の深さを選択します。" + +msgid "Select vendor prefix..." +msgstr "ベンダープレフィックスを選択..." + +msgid "Semibold" +msgstr "セミボールド" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"ベースとなるインターフェースのフォントサイズをピクセル単位で設定します。ほとんどのテーマテキストは、Fluent " +"CSS変数を通じてこの値からスケールされます。推奨範囲: 12-18px。" + +msgid "Show %s in the menu" +msgstr "%s をメニューに表示" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "設定適用が完了するまで、未適用のUCI変更をトップの適用状況メッセージに表示します。" + +msgid "Show top loading bar" +msgstr "上部のローディングバーを表示" + +msgid "Show UCI changes while applying configuration" +msgstr "設定適用中のUCI変更を表示します。" + +msgid "Sidebar background" +msgstr "サイドバーの背景" + +msgid "Sidebar width" +msgstr "サイドバーの幅" + +msgid "Slow" +msgstr "遅い" + +msgid "Small" +msgstr "小さい" + +msgid "Small (2px)" +msgstr "小さく (2px)" + +msgid "Software Update" +msgstr "ソフトウェア更新" + +msgid "Source code" +msgstr "ソースコード" + +msgid "Square (0px)" +msgstr "正方形 (0px)" + +msgid "Stable Channel" +msgstr "安定版チャンネル" + +msgid "Starting backend download..." +msgstr "バックエンドのダウンロードを開始します..." + +msgid "Starting update process..." +msgstr "更新プロセスを開始します..." + +msgid "Submit" +msgstr "送信" + +msgid "Switch theme mode" +msgstr "テーマモードを切り替える" + +msgid "Text direction" +msgstr "テキスト方向" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "保存されたメニューレイアウトが無効です。「デフォルトを復元」を選択するか、保存する前に編集してください。" + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "保存されたメニューレイアウトが無効です。デフォルトを復元するか、保存する前にレイアウトを編集してください。" + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "テーマがルーターにインストールされています。ログは下記に表示されます。Webインターフェースを再読み込みすると、RPCDの再起動後に変更が適用されます。" + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "テーマパッケージの準備が完了しました。インストールを実行しますか?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "このトークンには、権限/スコープは必要ありません (読み取り専用の公開アクセスで十分です)。" + +msgid "Theme settings" +msgstr "テーマ設定" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "テーマの更新に成功しました!RPCサービスを再読み込みします。5秒後にページをリフレッシュしてください。" + +msgid "Theme transition speed" +msgstr "テーマの遷移速度" + +msgid "Toggle fullscreen" +msgstr "全画面表示を切り替え" + +msgid "Triggering installation on router..." +msgstr "ルーターへのインストールを開始します..." + +msgid "Update channel" +msgstr "更新チャンネル" + +msgid "Update failed" +msgstr "更新に失敗しました" + +msgid "Upgrade Successful!" +msgstr "アップグレード成功!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Fluentカスタムセレクトドロップダウンを使用する" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "対応している場合、ブラウザのView Transition APIを使用してLuCIページのナビゲーションをアニメーション表示します。" + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "システム/ブラウザの設定に従うか、常にFluentテーマを固定されたライトまたはダークパレットでレンダリングします。" + +msgid "Username is required." +msgstr "ユーザー名は必須です。" + +msgid "valid HEX color value" +msgstr "有効なHEXカラー値" + +msgid "Warnings" +msgstr "警告" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "有効にすると、Fluentのアニメーションはブラウザまたはオペレーティングシステムの低減モーション設定に従います。" + +msgid "Width of the main navigation sidebar in pixels." +msgstr "メインナビゲーションサイドバーの幅をピクセル単位で指定します。" + +msgid "Word wrap" +msgstr "折り返し表示" + +msgid "You can create a new token at" +msgstr "新しいトークンは以下で作成できます:" + +msgid "Your theme is up to date!" +msgstr "テーマは最新です!" diff --git a/luci-theme-fluent/po/ko/fluent.po b/luci-theme-fluent/po/ko/fluent.po new file mode 100644 index 000000000..2a7762c4e --- /dev/null +++ b/luci-theme-fluent/po/ko/fluent.po @@ -0,0 +1,709 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: ko\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" + +msgid "%d second-level menus" +msgstr "%d 개의 하위 메뉴" + +msgid "A new version is available!" +msgstr "새 버전이 있습니다!" + +msgid "About" +msgstr "정보" + +msgid "Above bar, end" +msgstr "막대 위, 끝" + +msgid "Above bar, start" +msgstr "막대 위, 시작" + +msgid "Accent color" +msgstr "강조 색상" + +msgid "Add primary menu" +msgstr "기본 메뉴 추가" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"내장 컨트롤로 충분하지 않은 경우, 레이아웃, 타이포그래피, 전환 시간, 그림자 등을 조정하고 사용자 정의 CSS 변수 또는 규칙을 " +"추가합니다." + +msgid "Advanced" +msgstr "고급 설정" + +msgid "Advanced MAC Selector" +msgstr "고급 MAC 선택기" + +msgid "and %d more..." +msgstr "및 %d개 더..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "네이티브 LuCI 탭과 테마 적용된 탭 메뉴 간 전환 시 활성 밑줄 애니메이션을 표시합니다." + +msgid "Animation" +msgstr "애니메이션" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"API 요청 제한 초과. 계속하려면 GitHub 토큰을 입력하세요. 이 토큰은 프런트엔드 요청에만 사용되며 라우터 백엔드에 저장되지 " +"않습니다." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "표준 버튼, 입력 필드, 선택 상자 및 유사한 폼 컨트롤에 적용됩니다." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "%d개의 잘못되었거나 설치되지 않은 메뉴 항목 또는 규칙을 정리하시겠습니까?" + +msgid "Author" +msgstr "작성자" + +msgid "Auto-scroll" +msgstr "자동 스크롤" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "자동 (아랍어/페르시아어 지역 설정만 해당)" + +msgid "Backdrop blur radius" +msgstr "배경 블러 반경" + +msgid "Backend (GHProxy Acceleration)" +msgstr "백엔드 (GHProxy 가속)" + +msgid "Backend (Official GitHub)" +msgstr "백엔드 (공식 GitHub)" + +msgid "Base font size" +msgstr "기본 글꼴 크기" + +msgid "Below bar, end" +msgstr "막대 아래, 끝" + +msgid "Below bar, start" +msgstr "막대 아래, 시작" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "다크 모드에서 로그인 카드 뒤쪽의 블러 반경 (픽셀). 블러를 사용하지 않으려면 0을 입력하세요." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "라이트 모드에서 로그인 카드 뒤쪽의 블러 반경 (픽셀). 블러를 사용하지 않으려면 0을 입력하세요." + +msgid "Cancel" +msgstr "취소" + +msgid "Card background" +msgstr "카드 배경" + +msgid "Card shadow" +msgstr "카드 그림자" + +msgid "Check for updates" +msgstr "업데이트 확인" + +msgid "Checking for updates..." +msgstr "업데이트 확인 중..." + +msgid "Choose color" +msgstr "색상 선택" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"인증 및 로그인 페이지의 문서 방향을 선택합니다. 자동 모드는 아랍어 및 페르시아어 로케일 코드(ar, ar_*, ar-*, fa, " +"fa_*, fa-*)를 RTL로 처리하고, 누락되거나 인식되지 않는 로케일에 대해서는 LTR로 기본 설정됩니다." + +msgid "Clean up" +msgstr "정리하기" + +msgid "Clean up invalid items" +msgstr "잘못된 항목 정리" + +msgid "Clean up invalid items (%d)" +msgstr "잘못된 항목 정리 (%d)" + +msgid "Close" +msgstr "닫기" + +msgid "Color mode" +msgstr "색상 모드" + +msgid "Colors" +msgstr "색상" + +msgid "Comfortable (42px)" +msgstr "편안함 (42px)" + +msgid "Compact (32px)" +msgstr "압축 (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"색상 모드, 강조 색상, 레이아웃 크기 조정, 애니메이션 동작, 로그인 페이지 모양 및 luci-theme-fluent용 고급 CSS " +"설정을 구성합니다." + +msgid "Confirm" +msgstr "확인" + +msgid "Confirm Installation" +msgstr "설치 확인" + +msgid "Continue" +msgstr "계속" + +msgid "Control height" +msgstr "컨트롤 높이" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "메인 탐색 레이블 및 관련 테마 텍스트 강조에 사용되는 글꼴 두께를 제어합니다." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "카드, 버튼, 입력 필드 등 Fluent UI 요소에 사용되는 공통 코너 반경 값을 제어합니다." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "메뉴, 헤더 등 테마 애니메이션에 사용되는 공통 Fluent 전환 시간을 제어합니다." + +msgid "Copied" +msgstr "복사됨" + +msgid "Copy log" +msgstr "로그 복사" + +msgid "Corner radius" +msgstr "코너 반경" + +msgid "Current / Pending MAC Address" +msgstr "현재 / 대기 중인 MAC 주소" + +msgid "Custom CSS" +msgstr "사용자 정의 CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "로그인 페이지 배경, 카드 불투명도 및 라이트 모드 및 다크 모드의 블러 반경을 사용자 정의합니다." + +msgid "Dark mode" +msgstr "다크 모드" + +msgid "Delete primary menu" +msgstr "기본 메뉴 삭제" + +msgid "Disabled" +msgstr "사용 안 함" + +msgid "Disconnects" +msgstr "연결 끊김" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "페이지 로딩 및 전환 시, 테마 적용된 로딩 표시기를 화면 상단에 표시합니다." + +msgid "Download and Install" +msgstr "다운로드 및 설치" + +msgid "Download log" +msgstr "로그 다운로드" + +msgid "Downloaded" +msgstr "다운로드 완료" + +msgid "Downloading on router" +msgstr "라우터에서 다운로드 중" + +msgid "Drag to move second-level menu" +msgstr "하위 메뉴를 드래그하여 이동" + +msgid "Drag to reorder primary menu" +msgstr "기본 메뉴 순서를 변경하려면 드래그" + +msgid "Drop second-level menus here" +msgstr "여기에 하위 메뉴를 추가합니다." + +msgid "Edit" +msgstr "편집" + +msgid "Enable page transition animation" +msgstr "페이지 전환 애니메이션 활성화" + +msgid "Enable tab underline animation" +msgstr "탭 하단 선 애니메이션 활성화" + +msgid "Errors" +msgstr "오류" + +msgid "Expecting: %s" +msgstr "예상 값: %s" + +msgid "Extra large (12px)" +msgstr "매우 큼 (12px)" + +msgid "Failed to check for updates" +msgstr "업데이트 확인 실패" + +msgid "Failed to fetch current theme version." +msgstr "현재 테마 버전 정보를 가져오는데 실패했습니다." + +msgid "Fast" +msgstr "빠름" + +msgid "Finished" +msgstr "완료" + +msgid "Fix primary menu title errors before saving." +msgstr "저장하기 전에 기본 메뉴 제목 오류를 수정하십시오." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "LuCI용 Fluent Design 2 테마" + +msgid "Fluent Theme" +msgstr "Fluent 테마" + +msgid "Fluent theme settings" +msgstr "Fluent 테마 설정" + +msgid "Follow system" +msgstr "시스템 설정 따르기" + +msgid "Force dark mode" +msgstr "다크 모드 강제 적용" + +msgid "Force light mode" +msgstr "라이트 모드 강제 적용" + +msgid "Force LTR" +msgstr "왼쪽에서 오른쪽으로 (LTR) 강제 적용" + +msgid "Force RTL" +msgstr "오른쪽에서 왼쪽으로 (RTL) 강제 적용" + +msgid "General" +msgstr "일반 설정" + +msgid "GitHub Token Required" +msgstr "GitHub 토큰 필요" + +msgid "Header height" +msgstr "헤더 높이" + +msgid "Height of the top header bar in pixels." +msgstr "상단 헤더 바의 높이를 픽셀 단위로 지정합니다." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "다크 모드에서 인터페이스가 표시될 때 사용되는 주요 Fluent 강조 색상의 HEX 코드입니다." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "라이트 모드에서 인터페이스가 표시될 때 사용되는 주요 Fluent 강조 색상의 HEX 코드입니다." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "다크 모드에서 컨테이너/카드 요소에 사용되는 HEX 색상입니다." + +msgid "HEX color used for container/card elements in light mode." +msgstr "라이트 모드에서 컨테이너/카드 요소에 사용되는 HEX 색상입니다." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "다크 모드에서 인터페이스가 표시될 때 진행률 표시줄 레이블에 사용되는 HEX 색상입니다." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "라이트 모드에서 인터페이스가 표시될 때 진행률 표시줄 레이블에 사용되는 HEX 색상입니다." + +msgid "HEX color used for the main page background in dark mode." +msgstr "다크 모드에서 메인 페이지 배경에 사용되는 HEX 색상입니다." + +msgid "HEX color used for the main page background in light mode." +msgstr "라이트 모드에서 메인 페이지 배경에 사용되는 HEX 색상입니다." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "다크 모드에서 탐색 사이드바에 사용되는 HEX 색상." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "라이트 모드에서 탐색 사이드바에 사용되는 HEX 색상." + +msgid "Installation cancelled by user." +msgstr "사용자에 의해 설치가 취소되었습니다." + +msgid "Installed version" +msgstr "설치된 버전" + +msgid "Installing package" +msgstr "패키지 설치 중" + +msgid "Keep action bar visible while scrolling" +msgstr "스크롤 시 작업 표시줄을 계속 보이게 합니다." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "스크롤 시 저장, 적용 및 초기화 작업을 화면 하단에 고정합니다." + +msgid "Large" +msgstr "큼" + +msgid "Large (8px)" +msgstr "큼 (8px)" + +msgid "Light mode" +msgstr "라이트 모드" + +msgid "Line %d" +msgstr "%d번째 줄" + +msgid "Lines" +msgstr "줄 수" + +msgid "Login" +msgstr "로그인" + +msgid "Login card opacity" +msgstr "로그인 카드 불투명도" + +msgid "Login page" +msgstr "로그인 페이지" + +msgid "Medium" +msgstr "중간 크기" + +msgid "Medium (4px)" +msgstr "중간 크기 (4px)" + +msgid "Menu" +msgstr "메뉴" + +msgid "Menu title cannot be empty." +msgstr "메뉴 제목은 비워둘 수 없습니다." + +msgid "Navigation font weight" +msgstr "탐색 글꼴 두께" + +msgid "New primary menu" +msgstr "새로운 기본 메뉴" + +msgid "New primary menu %d" +msgstr "새로운 기본 메뉴 %d" + +msgid "Next" +msgstr "다음" + +msgid "Nightly build available (reinstallation check)." +msgstr "Nightly 빌드를 사용할 수 있습니다 (재설치 확인)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Nightly 채널 (사전 공개)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "이 릴리스에서 시스템 아키텍처에 맞는 패키지 파일을 찾을 수 없습니다." + +msgid "None" +msgstr "없음" + +msgid "Normal" +msgstr "일반" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "다크 모드에서 로그인 카드 불투명도. 0은 완전히 투명하고 1은 완전히 불투명합니다." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "라이트 모드에서 로그인 카드 불투명도. 0은 완전히 투명하고 1은 완전히 불투명합니다." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Fluent 헤더 템플릿에 추가되는 CSS입니다. --fluent-* 변수 재정의 또는 별도의 옵션으로 제공되지 않는 페이지별 수정 " +"사항을 위해 사용하십시오." + +msgid "Package format" +msgstr "패키지 형식" + +msgid "Page background" +msgstr "페이지 배경" + +msgid "Please enter your password." +msgstr "비밀번호를 입력하세요." + +msgid "Please enter your username." +msgstr "사용자 이름을 입력하세요." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "진행률 표시 막대의 레이블 위치는 막대에 상대적입니다. 시작/끝 정렬은 텍스트 방향(LTR 또는 RTL)을 따릅니다." + +msgid "Preset Vendor / Generator" +msgstr "미리 설정된 공급업체 / 생성기" + +msgid "Primary menu title" +msgstr "메인 메뉴 제목" + +msgid "Primary menu titles cannot be empty." +msgstr "메인 메뉴 제목은 비워둘 수 없습니다." + +msgid "Primary menu titles must be unique." +msgstr "메인 메뉴 제목은 고유해야 합니다." + +msgid "Progress bar text color" +msgstr "진행률 표시줄 텍스트 색상" + +msgid "Progress bar text position" +msgstr "진행률 표시 막대 텍스트 위치" + +msgid "Random Local MAC" +msgstr "임의 로컬 MAC 주소" + +msgid "Randomize MAC" +msgstr "MAC 주소 무작위 변경" + +msgid "Reload Web Interface" +msgstr "웹 인터페이스 새로 고침" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "네이티브 선택 요소를 테마의 사용자 정의 드롭다운 위젯으로 대체합니다." + +msgid "Respect reduced-motion preference" +msgstr "화면 전환 효과 감소 옵션을 준수합니다." + +msgid "Restore %s to its original menu name" +msgstr "%s를 원래 메뉴 이름으로 복원합니다." + +msgid "Restore %s to its original menu position" +msgstr "%s를 원래 위치로 복원합니다." + +msgid "Restore defaults" +msgstr "기본값 복원" + +msgid "Restore to original title" +msgstr "원래 제목으로 복원" + +msgid "Retrying with token..." +msgstr "토큰을 사용하여 다시 시도 중..." + +msgid "Rounded (6px)" +msgstr "둥글게 (6px)" + +msgid "Save" +msgstr "저장" + +msgid "Scroll to bottom" +msgstr "맨 아래로 스크롤" + +msgid "Scroll to top" +msgstr "맨 위로 이동" + +msgid "Search menu items" +msgstr "메뉴 항목 검색" + +msgid "Search menu..." +msgstr "메뉴 검색..." + +msgid "Second-level menu title" +msgstr "세부 메뉴 제목" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "테마 적용된 카드 및 패널에 적용되는 그림자 깊이를 선택합니다." + +msgid "Select vendor prefix..." +msgstr "벤더 접두사 선택..." + +msgid "Semibold" +msgstr "세미볼드" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"기본 인터페이스 글꼴 크기를 픽셀 단위로 설정합니다. 대부분의 테마 텍스트는 Fluent CSS 변수를 통해 이 값에서 조정됩니다. 권장 " +"범위: 12-18px." + +msgid "Show %s in the menu" +msgstr "%s를 메뉴에 표시합니다." + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "구성 적용이 완료될 때까지 최상단 적용 상태 메시지에 대기 중인 UCI 변경 사항을 표시합니다." + +msgid "Show top loading bar" +msgstr "상단 로딩 표시줄 표시" + +msgid "Show UCI changes while applying configuration" +msgstr "구성을 적용하는 동안 UCI 변경 사항을 표시합니다." + +msgid "Sidebar background" +msgstr "사이드바 배경" + +msgid "Sidebar width" +msgstr "사이드바 너비" + +msgid "Slow" +msgstr "느림" + +msgid "Small" +msgstr "작게" + +msgid "Small (2px)" +msgstr "작게 (2px)" + +msgid "Software Update" +msgstr "소프트웨어 업데이트" + +msgid "Source code" +msgstr "소스 코드" + +msgid "Square (0px)" +msgstr "정사각형 (0px)" + +msgid "Stable Channel" +msgstr "안정 채널" + +msgid "Starting backend download..." +msgstr "백엔드 다운로드 시작..." + +msgid "Starting update process..." +msgstr "업데이트 프로세스 시작..." + +msgid "Submit" +msgstr "제출" + +msgid "Switch theme mode" +msgstr "테마 모드 전환" + +msgid "Text direction" +msgstr "텍스트 방향" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "저장된 메뉴 레이아웃이 유효하지 않습니다. \"기본값 복원\"을 선택하거나 저장하기 전에 편집하십시오." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "저장된 메뉴 레이아웃이 유효하지 않습니다. 기본값을 복원하거나 저장하기 전에 레이아웃을 편집하십시오." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "테마가 라우터에 설치 중입니다. 로그는 아래에 표시됩니다. 웹 인터페이스를 다시 로드하면 RPCD가 재시작된 후 변경 사항이 적용됩니다." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "테마 패키지가 준비되었습니다. 설치를 진행하시겠습니까?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "토큰은 권한/범위 부여가 필요하지 않습니다 (읽기 전용 공개 액세스가 충분합니다)." + +msgid "Theme settings" +msgstr "테마 설정" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "테마 업데이트 성공! RPC 서비스를 다시 로드합니다. 페이지를 5초 후에 새로 고쳐주세요." + +msgid "Theme transition speed" +msgstr "테마 전환 속도" + +msgid "Toggle fullscreen" +msgstr "전체 화면 전환" + +msgid "Triggering installation on router..." +msgstr "라우터에서 설치를 시작합니다..." + +msgid "Update channel" +msgstr "업데이트 채널" + +msgid "Update failed" +msgstr "업데이트 실패" + +msgid "Upgrade Successful!" +msgstr "업그레이드 성공!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Fluent 사용자 정의 선택 드롭다운 사용" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "지원되는 경우, LuCI 페이지 간 탐색 시 브라우저의 View Transition API를 사용하여 애니메이션을 적용합니다." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "시스템/브라우저 설정을 사용하거나, 항상 Fluent 테마를 고정된 밝은 또는 어두운 색상으로 표시합니다." + +msgid "Username is required." +msgstr "사용자 이름이 필요합니다." + +msgid "valid HEX color value" +msgstr "유효한 HEX 색상 값" + +msgid "Warnings" +msgstr "경고" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "활성화하면, Fluent 애니메이션은 브라우저 또는 운영 체제의 움직임 감소 설정을 따릅니다." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "주요 탐색 사이드바의 너비 (픽셀 단위)." + +msgid "Word wrap" +msgstr "단어 줄 바꿈" + +msgid "You can create a new token at" +msgstr "새 토큰은 다음에서 생성할 수 있습니다:" + +msgid "Your theme is up to date!" +msgstr "귀하의 테마는 최신 버전입니다!" diff --git a/luci-theme-fluent/po/pl/fluent.po b/luci-theme-fluent/po/pl/fluent.po new file mode 100644 index 000000000..6b8e69853 --- /dev/null +++ b/luci-theme-fluent/po/pl/fluent.po @@ -0,0 +1,771 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: pl\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" + +msgid "%d second-level menus" +msgstr "%d podmenu" + +msgid "A new version is available!" +msgstr "Dostępna jest nowa wersja!" + +msgid "About" +msgstr "O programie" + +msgid "Above bar, end" +msgstr "Powyżej paska, koniec" + +msgid "Above bar, start" +msgstr "Powyżej paska, początek" + +msgid "Accent color" +msgstr "Kolor akcentu" + +msgid "Add primary menu" +msgstr "Dodaj główne menu" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Dostosuj układ, typografię, czas trwania animacji, cienie oraz dodawaj " +"własne zmienne CSS lub reguły, gdy wbudowane opcje są niewystarczające." + +msgid "Advanced" +msgstr "Zaawansowane" + +msgid "Advanced MAC Selector" +msgstr "Zaawansowany wybór adresów MAC" + +msgid "and %d more..." +msgstr "i %d więcej..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Animuj podświetlenie aktywnej zakładki podczas przełączania między natywnymi " +"zakładkami LuCI a menu zakładek z motywem." + +msgid "Animation" +msgstr "Animacja" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Osiągnięto limit szybkości wywołań API. Wprowadź token GitHub, aby " +"kontynuować. Ten token jest używany tylko do żądań frontendowych i nie " +"zostanie zapisany w backendzie routera." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Dotyczy standardowych przycisków, pól wprowadzania, list wyboru oraz " +"podobnych elementów sterujących formularzami w całym motywie." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Czy na pewno chcesz usunąć %d nieprawidłowych/niezainstalowanych elementów " +"menu lub reguł?" + +msgid "Author" +msgstr "Autor" + +msgid "Auto-scroll" +msgstr "Automatyczne przewijanie" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Automatyczne (tylko dla lokalizacji arabskich/perskich)" + +msgid "Backdrop blur radius" +msgstr "Promień rozmycia tła" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Backend (Przyspieszenie GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "Backend (Oficjalny GitHub)" + +msgid "Base font size" +msgstr "Podstawowy rozmiar czcionki" + +msgid "Below bar, end" +msgstr "Poniżej paska, koniec" + +msgid "Below bar, start" +msgstr "Poniżej paska, początek" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Promień rozmycia w pikselach za kartą logowania w trybie ciemnym. Użyj 0, " +"aby wyłączyć rozmycie." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Promień rozmycia w pikselach za kartą logowania w trybie jasnym. Użyj 0, aby " +"wyłączyć rozmycie." + +msgid "Cancel" +msgstr "Anuluj" + +msgid "Card background" +msgstr "Tło karty" + +msgid "Card shadow" +msgstr "Cień karty" + +msgid "Check for updates" +msgstr "Sprawdź aktualizacje" + +msgid "Checking for updates..." +msgstr "Sprawdzanie dostępności aktualizacji..." + +msgid "Choose color" +msgstr "Wybierz kolor" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Wybierz kierunek tekstu dla stron uwierzytelniania i logowania. Tryb " +"automatyczny rozpoznaje kody języków arabskiego i perskiego (ar, ar_*, ar-*, " +"fa, fa_*, fa-*) i ustawia układ RTL, a w przypadku brakujących lub " +"nieznanych lokalizacji przełącza się na LTR." + +msgid "Clean up" +msgstr "Oczyść" + +msgid "Clean up invalid items" +msgstr "Usuń nieprawidłowe elementy" + +msgid "Clean up invalid items (%d)" +msgstr "Usuń nieprawidłowe elementy (%d)" + +msgid "Close" +msgstr "Zamknij" + +msgid "Color mode" +msgstr "Tryb koloru" + +msgid "Colors" +msgstr "Kolory" + +msgid "Comfortable (42px)" +msgstr "Wygodny (42px)" + +msgid "Compact (32px)" +msgstr "Kompaktowy (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Konfiguruj tryb koloru, kolory akcentu, rozmiar układu, zachowanie animacji, " +"wygląd strony logowania oraz zaawansowane nadpisy CSS dla luci-theme-fluent." + +msgid "Confirm" +msgstr "Potwierdź" + +msgid "Confirm Installation" +msgstr "Potwierdź instalację" + +msgid "Continue" +msgstr "Kontynuuj" + +msgid "Control height" +msgstr "Wysokość elementów sterujących" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Określa grubość czcionki używaną przez etykiety nawigacji głównej i " +"powiązane elementy tekstowe motywu." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Kontroluje współdzielone tokeny promienia narożnika Fluent, używane przez " +"karty, przyciski, pola wprowadzania i inne elementy interfejsu użytkownika." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Kontroluje współdzielone czasy trwania animacji Fluent, używane przez menu, " +"nagłówek i inne animacje motywu." + +msgid "Copied" +msgstr "Skopiowano" + +msgid "Copy log" +msgstr "Kopiuj log" + +msgid "Corner radius" +msgstr "Promień narożnika" + +msgid "Current / Pending MAC Address" +msgstr "Aktualny / Oczekujący adres MAC" + +msgid "Custom CSS" +msgstr "Własny CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Dostosuj tło strony logowania, przezroczystość kart i promień rozmycia dla " +"trybów jasnego i ciemnego." + +msgid "Dark mode" +msgstr "Tryb ciemny" + +msgid "Delete primary menu" +msgstr "Usuń główne menu" + +msgid "Disabled" +msgstr "Wyłączone" + +msgid "Disconnects" +msgstr "Rozłączenia" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Wyświetl motywowany wskaźnik ładowania na górnej krawędzi podczas ładowania " +"stron i przejść." + +msgid "Download and Install" +msgstr "Pobierz i zainstaluj" + +msgid "Download log" +msgstr "Pobierz log" + +msgid "Downloaded" +msgstr "Pobrane" + +msgid "Downloading on router" +msgstr "Pobieranie na routerze" + +msgid "Drag to move second-level menu" +msgstr "Przeciągnij, aby przenieść podmenu" + +msgid "Drag to reorder primary menu" +msgstr "Przeciągnij, aby zmienić kolejność głównych menu" + +msgid "Drop second-level menus here" +msgstr "Upuść tutaj podmenu" + +msgid "Edit" +msgstr "Edytuj" + +msgid "Enable page transition animation" +msgstr "Włącz animację przejścia między stronami" + +msgid "Enable tab underline animation" +msgstr "Włącz animację podkreślenia zakładki" + +msgid "Errors" +msgstr "Błędy" + +msgid "Expecting: %s" +msgstr "Oczekiwane: %s" + +msgid "Extra large (12px)" +msgstr "Bardzo duże (12px)" + +msgid "Failed to check for updates" +msgstr "Nie udało się sprawdzić dostępności aktualizacji" + +msgid "Failed to fetch current theme version." +msgstr "Nie udało się pobrać bieżącej wersji motywu." + +msgid "Fast" +msgstr "Szybko" + +msgid "Finished" +msgstr "Zakończono" + +msgid "Fix primary menu title errors before saving." +msgstr "Napraw błędy tytułów głównych menu przed zapisaniem." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Motyw Fluent Design 2 dla LuCI" + +msgid "Fluent Theme" +msgstr "Motyw Fluent" + +msgid "Fluent theme settings" +msgstr "Ustawienia motywu Fluent" + +msgid "Follow system" +msgstr "Dostosuj do systemu" + +msgid "Force dark mode" +msgstr "Wymuś tryb ciemny" + +msgid "Force light mode" +msgstr "Wymuś tryb jasny" + +msgid "Force LTR" +msgstr "Wymuś układ lewo-do-prawo (LTR)" + +msgid "Force RTL" +msgstr "Wymuś układ prawo-do-lewo (RTL)" + +msgid "General" +msgstr "Ogólne" + +msgid "GitHub Token Required" +msgstr "Wymagany token GitHub" + +msgid "Header height" +msgstr "Wysokość nagłówka" + +msgid "Height of the top header bar in pixels." +msgstr "Wysokość górnego paska nagłówka w pikselach." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Kolor HEX używany jako główny akcent Fluent, gdy interfejs jest wyświetlany " +"w trybie ciemnym." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Kolor HEX używany jako główny akcent Fluent, gdy interfejs jest wyświetlany " +"w trybie jasnym." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "Kolor HEX używany dla elementów kontenerów/kart w trybie ciemnym." + +msgid "HEX color used for container/card elements in light mode." +msgstr "Kolor HEX używany dla elementów kontenerów/kart w trybie jasnym." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"Kolor HEX używany dla etykiet pasków postępu, gdy interfejs jest wyświetlany " +"w trybie ciemnym." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Kolor HEX używany dla etykiet pasków postępu, gdy interfejs jest wyświetlany " +"w trybie jasnym." + +msgid "HEX color used for the main page background in dark mode." +msgstr "Kolor HEX używany jako tło głównej strony w trybie ciemnym." + +msgid "HEX color used for the main page background in light mode." +msgstr "Kolor HEX używany jako tło głównej strony w trybie jasnym." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "Kolor HEX używany dla paska nawigacji w trybie ciemnym." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "Kolor HEX używany dla paska nawigacji w trybie jasnym." + +msgid "Installation cancelled by user." +msgstr "Instalacja anulowana przez użytkownika." + +msgid "Installed version" +msgstr "Zainstalowana wersja" + +msgid "Installing package" +msgstr "Instalowanie pakietu" + +msgid "Keep action bar visible while scrolling" +msgstr "Zachowaj pasek akcji podczas przewijania." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Utrzymuj przyciski Zapisz, Zastosuj i Reset na dole okna podczas " +"przewijania." + +msgid "Large" +msgstr "Duży" + +msgid "Large (8px)" +msgstr "Duży (8px)" + +msgid "Light mode" +msgstr "Tryb jasny" + +msgid "Line %d" +msgstr "Linia %d" + +msgid "Lines" +msgstr "Linii" + +msgid "Login" +msgstr "Logowanie" + +msgid "Login card opacity" +msgstr "Przezroczystość karty logowania" + +msgid "Login page" +msgstr "Strona logowania" + +msgid "Medium" +msgstr "Średni" + +msgid "Medium (4px)" +msgstr "Średni (4px)" + +msgid "Menu" +msgstr "Menu" + +msgid "Menu title cannot be empty." +msgstr "Tytuł menu nie może być pusty." + +msgid "Navigation font weight" +msgstr "Waga czcionki nawigacji" + +msgid "New primary menu" +msgstr "Nowe główne menu" + +msgid "New primary menu %d" +msgstr "Nowe główne menu %d" + +msgid "Next" +msgstr "Następny" + +msgid "Nightly build available (reinstallation check)." +msgstr "Dostępna wersja Nightly (sprawdzenie konieczności ponownej instalacji)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Kanał Nightly (wersja przedpremierowa)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" +"Nie znaleziono odpowiedniego pakietu dla architektury Twojego systemu w tej " +"wersji." + +msgid "None" +msgstr "Brak" + +msgid "Normal" +msgstr "Normalny" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Przezroczystość karty logowania w trybie ciemnym. 0 to całkowita " +"przezroczystość, a 1 to całkowita nieprzezroczystość." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Przezroczystość karty logowania w trybie jasnym. 0 to całkowita " +"przezroczystość, a 1 to całkowita nieprzezroczystość." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Opcjonalny surowy CSS wstrzykiwany do szablonu nagłówka Fluent. Użyj tego do " +"dodatkowych nadpisywań zmiennych --fluent-* lub modyfikacji specyficznych " +"dla strony, które nie są dostępne jako dedykowane opcje." + +msgid "Package format" +msgstr "Format pakietu" + +msgid "Page background" +msgstr "Tło strony" + +msgid "Please enter your password." +msgstr "Wprowadź swoje hasło." + +msgid "Please enter your username." +msgstr "Wprowadź swoją nazwę użytkownika." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Położenie etykiet paska postępu względem paska. Wyjustowanie początku/końca " +"podąża za kierunkiem tekstu (LTR lub RTL)." + +msgid "Preset Vendor / Generator" +msgstr "Gotowy zestaw dostawcy / generatora" + +msgid "Primary menu title" +msgstr "Tytuł głównego menu" + +msgid "Primary menu titles cannot be empty." +msgstr "Tytuły w głównym menu nie mogą być puste." + +msgid "Primary menu titles must be unique." +msgstr "Tytuły w głównym menu muszą być unikalne." + +msgid "Progress bar text color" +msgstr "Kolor tekstu paska postępu" + +msgid "Progress bar text position" +msgstr "Pozycja tekstu na pasku postępu" + +msgid "Random Local MAC" +msgstr "Losowy lokalny adres MAC" + +msgid "Randomize MAC" +msgstr "Losuj adres MAC" + +msgid "Reload Web Interface" +msgstr "Przeładuj interfejs webowy" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "Zastąp natywne elementy wyboru niestandardowym elementem rozwijanym motywu." + +msgid "Respect reduced-motion preference" +msgstr "Uwzględnij preferencje dotyczące ograniczenia animacji." + +msgid "Restore %s to its original menu name" +msgstr "Przywróć %s do oryginalnej nazwy menu" + +msgid "Restore %s to its original menu position" +msgstr "Przywróć %s do oryginalnej pozycji w menu" + +msgid "Restore defaults" +msgstr "Przywróć domyślne ustawienia" + +msgid "Restore to original title" +msgstr "Przywróć oryginalny tytuł" + +msgid "Retrying with token..." +msgstr "Ponawianie próby z tokenem..." + +msgid "Rounded (6px)" +msgstr "Zaokrąglone (6px)" + +msgid "Save" +msgstr "Zapisz" + +msgid "Scroll to bottom" +msgstr "Przewiń na dół" + +msgid "Scroll to top" +msgstr "Przewiń do góry" + +msgid "Search menu items" +msgstr "Wyszukaj pozycje menu" + +msgid "Search menu..." +msgstr "Wyszukuj w menu..." + +msgid "Second-level menu title" +msgstr "Tytuł podmenu" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "Wybierz głębokość cienia stosowaną do kart i paneli motywu." + +msgid "Select vendor prefix..." +msgstr "Wybierz prefiks dostawcy..." + +msgid "Semibold" +msgstr "Półpogrubienie" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Ustawia podstawowy rozmiar czcionki interfejsu w pikselach. Większość tekstu " +"motywu skaluje się od tej wartości za pomocą zmiennych CSS Fluent. Zalecany " +"zakres: 12-18px." + +msgid "Show %s in the menu" +msgstr "Pokaż %s w menu" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Wyświetlaj oczekujące zmiany UCI w górnym komunikacie o statusie, aż do " +"zakończenia zastosowania konfiguracji." + +msgid "Show top loading bar" +msgstr "Pokaż pasek ładowania na górze" + +msgid "Show UCI changes while applying configuration" +msgstr "Wyświetlaj zmiany UCI podczas stosowania konfiguracji." + +msgid "Sidebar background" +msgstr "Tło paska bocznego" + +msgid "Sidebar width" +msgstr "Szerokość paska bocznego" + +msgid "Slow" +msgstr "Wolno" + +msgid "Small" +msgstr "Mały" + +msgid "Small (2px)" +msgstr "Mały (2px)" + +msgid "Software Update" +msgstr "Aktualizacja oprogramowania" + +msgid "Source code" +msgstr "Kod źródłowy" + +msgid "Square (0px)" +msgstr "Kwadrat (0px)" + +msgid "Stable Channel" +msgstr "Kanał stabilny" + +msgid "Starting backend download..." +msgstr "Rozpoczyna pobieranie pakietu..." + +msgid "Starting update process..." +msgstr "Rozpoczyna proces aktualizacji..." + +msgid "Submit" +msgstr "Zatwierdź" + +msgid "Switch theme mode" +msgstr "Zmień tryb motywu" + +msgid "Text direction" +msgstr "Kierunek tekstu" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"Zapisany układ menu jest nieprawidłowy. Wybierz \"Przywróć domyślne " +"ustawienia\" lub dokonaj edycji przed zapisaniem." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"Zapisany układ menu jest nieprawidłowy. Przywróć domyślne ustawienia lub " +"edytuj układ przed zapisaniem." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Motyw jest instalowany na routerze. Logi są wyświetlane poniżej. Ponowne " +"załadowanie interfejsu webowego zastosuje zmiany po ponownym uruchomieniu " +"RPCD." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "Pakiet motywu jest gotowy. Czy chcesz kontynuować instalację?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Token NIE wymaga żadnych uprawnień/zakresów (wystarczy dostęp publiczny " +"tylko do odczytu)." + +msgid "Theme settings" +msgstr "Ustawienia motywu" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Motyw został pomyślnie zaktualizowany! Próba ponownego uruchomienia usługi " +"RPC, odśwież stronę za 5 sekund." + +msgid "Theme transition speed" +msgstr "Prędkość animacji zmiany motywu" + +msgid "Toggle fullscreen" +msgstr "Przełącz tryb pełnoekranowy" + +msgid "Triggering installation on router..." +msgstr "Rozpoczyna instalację na routerze..." + +msgid "Update channel" +msgstr "Kanał aktualizacji" + +msgid "Update failed" +msgstr "Aktualizacja nie powiodła się" + +msgid "Upgrade Successful!" +msgstr "Aktualizacja zakończona pomyślnie!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Używaj niestandardowych list wyboru Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Wykorzystaj wbudowaną w przeglądarkę View Transition API do animowania " +"przejść między stronami LuCI, jeśli jest obsługiwana." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Użyj preferencji systemu/przeglądarki lub zawsze renderuj motyw Fluent z " +"ustaloną paletą kolorów (jasną lub ciemną)." + +msgid "Username is required." +msgstr "Nazwa użytkownika jest wymagana." + +msgid "valid HEX color value" +msgstr "poprawna wartość koloru w formacie HEX" + +msgid "Warnings" +msgstr "Ostrzeżenia" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Po włączeniu animacje Fluent uwzględniają preferencje dotyczące ograniczenia " +"ruchu ustawione w przeglądarce lub systemie operacyjnym." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Szerokość głównego paska nawigacyjnego w pikselach." + +msgid "Word wrap" +msgstr "Zawijanie tekstu" + +msgid "You can create a new token at" +msgstr "Możesz utworzyć nowy token pod adresem:" + +msgid "Your theme is up to date!" +msgstr "Twój motyw jest aktualny!" diff --git a/luci-theme-fluent/po/ru/fluent.po b/luci-theme-fluent/po/ru/fluent.po new file mode 100644 index 000000000..f09a640aa --- /dev/null +++ b/luci-theme-fluent/po/ru/fluent.po @@ -0,0 +1,764 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:37+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: ru\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" + +msgid "%d second-level menus" +msgstr "%d меню второго уровня" + +msgid "A new version is available!" +msgstr "Доступна новая версия!" + +msgid "About" +msgstr "О теме" + +msgid "Above bar, end" +msgstr "Над полосой, конец" + +msgid "Above bar, start" +msgstr "Над полосой, начало" + +msgid "Accent color" +msgstr "Акцентный цвет" + +msgid "Add primary menu" +msgstr "Добавить главное меню" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Настройте компоновку, типографику, длительность переходов, тени, а также " +"добавьте собственные CSS-переменные или правила, если встроенных параметров " +"недостаточно." + +msgid "Advanced" +msgstr "Дополнительно" + +msgid "Advanced MAC Selector" +msgstr "Расширенный выбор MAC-адресов" + +msgid "and %d more..." +msgstr "и ещё %d..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Анимировать активное подчёркивание при переключении между стандартными " +"вкладками LuCI и тематическими меню вкладок." + +msgid "Animation" +msgstr "Анимация" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Превышен лимит запросов API. Введите токен GitHub, чтобы продолжить. Этот " +"токен используется только для запросов фронтенда и не будет сохранён на " +"стороне роутера." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Применяется к стандартным кнопкам, полям ввода, спискам выбора и похожим " +"элементам формы во всей теме." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Вы уверены, что хотите удалить %d недействительных/неустановленных пунктов " +"меню или правил?" + +msgid "Author" +msgstr "Автор" + +msgid "Auto-scroll" +msgstr "Автопрокрутка" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Автоматически (только для арабских и персидских локалей)" + +msgid "Backdrop blur radius" +msgstr "Радиус размытия фона" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Серверная часть (ускорение через GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "Серверная часть (официальный GitHub)" + +msgid "Base font size" +msgstr "Базовый размер шрифта" + +msgid "Below bar, end" +msgstr "Под полосой, конец" + +msgid "Below bar, start" +msgstr "Под полосой, начало" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Радиус размытия в пикселях за карточкой входа в тёмном режиме. Используйте " +"0, чтобы отключить размытие." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Радиус размытия в пикселях за карточкой входа в светлом режиме. Используйте " +"0, чтобы отключить размытие." + +msgid "Cancel" +msgstr "Отмена" + +msgid "Card background" +msgstr "Фон карточки" + +msgid "Card shadow" +msgstr "Тень карточки" + +msgid "Check for updates" +msgstr "Проверить обновления" + +msgid "Checking for updates..." +msgstr "Проверка обновлений..." + +msgid "Choose color" +msgstr "Выбрать цвет" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Выберите направление текста для страниц аутентификации и входа. В " +"автоматическом режиме коды локалей арабского и персидского языков (ar, ar_*, " +"ar-*, fa, fa_*, fa-*) определяются как RTL, а для отсутствующих или " +"нераспознанных локалей используется LTR." + +msgid "Clean up" +msgstr "Очистить" + +msgid "Clean up invalid items" +msgstr "Очистить недействительные элементы" + +msgid "Clean up invalid items (%d)" +msgstr "Очистить недействительные элементы (%d)" + +msgid "Close" +msgstr "Закрыть" + +msgid "Color mode" +msgstr "Цветовой режим" + +msgid "Colors" +msgstr "Цвета" + +msgid "Comfortable (42px)" +msgstr "Удобный (42px)" + +msgid "Compact (32px)" +msgstr "Компактный (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Настройте цветовой режим, акцентные цвета, размеры интерфейса, поведение " +"анимации, внешний вид страницы входа и расширенные CSS-переопределения для " +"luci-theme-fluent." + +msgid "Confirm" +msgstr "Подтвердить" + +msgid "Confirm Installation" +msgstr "Подтвердить установку" + +msgid "Continue" +msgstr "Продолжить" + +msgid "Control height" +msgstr "Высота элементов управления" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Определяет насыщенность шрифта для основных навигационных подписей и " +"связанных акцентных текстов темы." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Определяет общие значения скругления Fluent, используемые карточками, " +"кнопками, полями ввода и другими элементами интерфейса." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Определяет общую длительность переходов Fluent для меню, верхней панели и " +"других анимаций темы." + +msgid "Copied" +msgstr "Скопировано" + +msgid "Copy log" +msgstr "Копировать журнал" + +msgid "Corner radius" +msgstr "Радиус скругления" + +msgid "Current / Pending MAC Address" +msgstr "Текущий / ожидающий MAC-адрес" + +msgid "Custom CSS" +msgstr "Пользовательский CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Настройте фон страницы входа, непрозрачность карточки и радиус размытия для " +"светлого и тёмного режимов." + +msgid "Dark mode" +msgstr "Тёмный режим" + +msgid "Delete primary menu" +msgstr "Удалить главное меню" + +msgid "Disabled" +msgstr "Отключено" + +msgid "Disconnects" +msgstr "Отключения" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Показывать тематический индикатор загрузки в верхней части страницы во время " +"загрузки и переходов." + +msgid "Download and Install" +msgstr "Скачать и установить" + +msgid "Download log" +msgstr "Загрузить журнал" + +msgid "Downloaded" +msgstr "Загружено" + +msgid "Downloading on router" +msgstr "Загрузка на роутере" + +msgid "Drag to move second-level menu" +msgstr "Перетащите для перемещения меню второго уровня" + +msgid "Drag to reorder primary menu" +msgstr "Перетащите для изменения порядка главного меню" + +msgid "Drop second-level menus here" +msgstr "Перетащите сюда меню второго уровня" + +msgid "Edit" +msgstr "Редактировать" + +msgid "Enable page transition animation" +msgstr "Включить анимацию переходов между страницами" + +msgid "Enable tab underline animation" +msgstr "Включить анимацию подчёркивания вкладок" + +msgid "Errors" +msgstr "Ошибки" + +msgid "Expecting: %s" +msgstr "Ожидается: %s" + +msgid "Extra large (12px)" +msgstr "Очень большой (12px)" + +msgid "Failed to check for updates" +msgstr "Не удалось проверить обновления" + +msgid "Failed to fetch current theme version." +msgstr "Не удалось получить текущую версию темы." + +msgid "Fast" +msgstr "Быстро" + +msgid "Finished" +msgstr "Завершено" + +msgid "Fix primary menu title errors before saving." +msgstr "Исправьте ошибки в заголовках главных меню перед сохранением." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Тема Fluent Design 2 для LuCI" + +msgid "Fluent Theme" +msgstr "Тема Fluent" + +msgid "Fluent theme settings" +msgstr "Настройки темы Fluent" + +msgid "Follow system" +msgstr "Следовать системе" + +msgid "Force dark mode" +msgstr "Всегда тёмный режим" + +msgid "Force light mode" +msgstr "Всегда светлый режим" + +msgid "Force LTR" +msgstr "Принудительно LTR" + +msgid "Force RTL" +msgstr "Принудительно RTL" + +msgid "General" +msgstr "Общие" + +msgid "GitHub Token Required" +msgstr "Требуется токен GitHub" + +msgid "Header height" +msgstr "Высота верхней панели" + +msgid "Height of the top header bar in pixels." +msgstr "Высота верхней панели в пикселях." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "HEX-цвет, используемый как основной акцентный цвет Fluent в тёмном режиме." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "HEX-цвет, используемый как основной акцентный цвет Fluent в светлом режиме." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "HEX-цвет для элементов контейнеров и карточек в тёмном режиме." + +msgid "HEX color used for container/card elements in light mode." +msgstr "HEX-цвет для элементов контейнеров и карточек в светлом режиме." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "HEX-цвет текста индикатора прогресса в тёмном режиме." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "HEX-цвет текста индикатора прогресса в светлом режиме." + +msgid "HEX color used for the main page background in dark mode." +msgstr "HEX-цвет фона основной страницы в тёмном режиме." + +msgid "HEX color used for the main page background in light mode." +msgstr "HEX-цвет фона основной страницы в светлом режиме." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "HEX-цвет боковой панели навигации в тёмном режиме." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "HEX-цвет боковой панели навигации в светлом режиме." + +msgid "Installation cancelled by user." +msgstr "Установка отменена пользователем." + +msgid "Installed version" +msgstr "Установленная версия" + +msgid "Installing package" +msgstr "Установка пакета" + +msgid "Keep action bar visible while scrolling" +msgstr "Оставлять панель действий видимой при прокрутке" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Фиксировать кнопки 'Сохранить', 'Применить' и 'Сброс' внизу экрана во время " +"прокрутки." + +msgid "Large" +msgstr "Большой" + +msgid "Large (8px)" +msgstr "Большой (8px)" + +msgid "Light mode" +msgstr "Светлый режим" + +msgid "Line %d" +msgstr "Строка %d" + +msgid "Lines" +msgstr "Строки" + +msgid "Login" +msgstr "Вход" + +msgid "Login card opacity" +msgstr "Непрозрачность карточки входа" + +msgid "Login page" +msgstr "Страница входа" + +msgid "Medium" +msgstr "Средний" + +msgid "Medium (4px)" +msgstr "Средний (4px)" + +msgid "Menu" +msgstr "Меню" + +msgid "Menu title cannot be empty." +msgstr "Название пункта меню не может быть пустым." + +msgid "Navigation font weight" +msgstr "Насыщенность шрифта навигации" + +msgid "New primary menu" +msgstr "Новое главное меню" + +msgid "New primary menu %d" +msgstr "Новое главное меню %d" + +msgid "Next" +msgstr "Далее" + +msgid "Nightly build available (reinstallation check)." +msgstr "Доступна nightly-сборка (проверка необходимости переустановки)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Канал Nightly (предварительный выпуск)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "В этом выпуске не найден подходящий пакет для архитектуры вашей системы." + +msgid "None" +msgstr "Нет" + +msgid "Normal" +msgstr "Обычно" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Непрозрачность карточки входа в тёмном режиме. 0 означает полностью " +"прозрачную карточку, 1 — полностью непрозрачную." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Непрозрачность карточки входа в светлом режиме. 0 означает полностью " +"прозрачную карточку, 1 — полностью непрозрачную." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Необязательный необработанный CSS, внедряемый в шаблон заголовка Fluent. " +"Используйте его для дополнительных переопределений переменных --fluent-* или " +"точечных настроек страниц, для которых нет отдельных параметров." + +msgid "Package format" +msgstr "Формат пакета" + +msgid "Page background" +msgstr "Фон страницы" + +msgid "Please enter your password." +msgstr "Введите пароль." + +msgid "Please enter your username." +msgstr "Введите имя пользователя." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Положение текста индикатора прогресса относительно полосы. Выравнивание " +"начала/конца следует направлению текста (слева направо или справа налево)." + +msgid "Preset Vendor / Generator" +msgstr "Предустановленный производитель / генератор" + +msgid "Primary menu title" +msgstr "Заголовок основного меню" + +msgid "Primary menu titles cannot be empty." +msgstr "Заголовки основного меню не могут быть пустыми." + +msgid "Primary menu titles must be unique." +msgstr "Заголовки основного меню должны быть уникальными." + +msgid "Progress bar text color" +msgstr "Цвет текста индикатора прогресса" + +msgid "Progress bar text position" +msgstr "Положение текста полосы прогресса" + +msgid "Random Local MAC" +msgstr "Случайный локальный MAC-адрес" + +msgid "Randomize MAC" +msgstr "Случайный MAC-адрес" + +msgid "Reload Web Interface" +msgstr "Перезагрузить веб-интерфейс" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" +"Заменить стандартные списки выбора пользовательским выпадающим компонентом " +"темы." + +msgid "Respect reduced-motion preference" +msgstr "Учитывать настройку уменьшения анимации" + +msgid "Restore %s to its original menu name" +msgstr "Восстановить исходное имя меню для %s" + +msgid "Restore %s to its original menu position" +msgstr "Восстановить исходное положение меню для %s" + +msgid "Restore defaults" +msgstr "Восстановить настройки по умолчанию" + +msgid "Restore to original title" +msgstr "Восстановить исходное название" + +msgid "Retrying with token..." +msgstr "Повторная попытка с токеном..." + +msgid "Rounded (6px)" +msgstr "Скруглённый (6px)" + +msgid "Save" +msgstr "Сохранить" + +msgid "Scroll to bottom" +msgstr "Прокрутить вниз" + +msgid "Scroll to top" +msgstr "Прокрутить вверх" + +msgid "Search menu items" +msgstr "Поиск по пунктам меню" + +msgid "Search menu..." +msgstr "Искать в меню..." + +msgid "Second-level menu title" +msgstr "Название второго уровня меню" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "Выберите глубину тени для тематических карточек и панелей." + +msgid "Select vendor prefix..." +msgstr "Выбрать префикс производителя..." + +msgid "Semibold" +msgstr "Полужирный" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Задаёт базовый размер шрифта интерфейса в пикселях. Большинство текстовых " +"элементов темы масштабируются от этого значения через CSS-переменные Fluent. " +"Рекомендуемый диапазон: 12-18px." + +msgid "Show %s in the menu" +msgstr "Показать %s в меню" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Отображать ожидающие изменения UCI в главном сообщении о статусе применения, " +"пока процесс применения конфигурации не завершится." + +msgid "Show top loading bar" +msgstr "Показывать верхнюю полосу загрузки" + +msgid "Show UCI changes while applying configuration" +msgstr "Показывать изменения UCI во время применения конфигурации" + +msgid "Sidebar background" +msgstr "Фон боковой панели" + +msgid "Sidebar width" +msgstr "Ширина боковой панели" + +msgid "Slow" +msgstr "Медленно" + +msgid "Small" +msgstr "Маленький" + +msgid "Small (2px)" +msgstr "Маленький (2px)" + +msgid "Software Update" +msgstr "Обновление ПО" + +msgid "Source code" +msgstr "Исходный код" + +msgid "Square (0px)" +msgstr "Квадрат (0px)" + +msgid "Stable Channel" +msgstr "Стабильный канал" + +msgid "Starting backend download..." +msgstr "Запуск загрузки на стороне роутера..." + +msgid "Starting update process..." +msgstr "Запуск процесса обновления..." + +msgid "Submit" +msgstr "Отправить" + +msgid "Switch theme mode" +msgstr "Переключить режим темы" + +msgid "Text direction" +msgstr "Направление текста" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"Сохраненная структура меню недействительна. Выберите \"Восстановить настройки " +"по умолчанию\" или внесите изменения перед сохранением." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"Сохраненная структура меню недействительна. Восстановите настройки по " +"умолчанию или отредактируйте структуру перед сохранением." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Тема устанавливается на ваш роутер. Журнал показан ниже. После перезапуска " +"RPCD перезагрузка веб-интерфейса применит изменения." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "Пакет темы готов. Продолжить установку?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Токену НЕ требуются дополнительные разрешения или области доступа " +"(достаточно публичного доступа только для чтения)." + +msgid "Theme settings" +msgstr "Настройки темы" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Тема успешно обновлена! Выполняется перезагрузка службы RPC, обновите " +"страницу через 5 секунд." + +msgid "Theme transition speed" +msgstr "Скорость переходов темы" + +msgid "Toggle fullscreen" +msgstr "Переключить полноэкранный режим" + +msgid "Triggering installation on router..." +msgstr "Запуск установки на роутере..." + +msgid "Update channel" +msgstr "Канал обновлений" + +msgid "Update failed" +msgstr "Обновление не удалось" + +msgid "Upgrade Successful!" +msgstr "Обновление прошло успешно!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Использовать пользовательские выпадающие списки Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Использовать View Transition API браузера для анимации переходов между " +"страницами LuCI, если это поддерживается." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Использовать системные или браузерные предпочтения либо всегда отображать " +"тему Fluent в фиксированной светлой или тёмной палитре." + +msgid "Username is required." +msgstr "Требуется имя пользователя." + +msgid "valid HEX color value" +msgstr "корректное значение HEX-цвета" + +msgid "Warnings" +msgstr "Предупреждения" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Если включено, анимации Fluent будут учитывать настройку уменьшения анимации " +"в браузере или операционной системе." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Ширина основной боковой панели навигации в пикселях." + +msgid "Word wrap" +msgstr "Перенос слов" + +msgid "You can create a new token at" +msgstr "Новый токен можно создать здесь:" + +msgid "Your theme is up to date!" +msgstr "У вас уже установлена последняя версия темы!" diff --git a/luci-theme-fluent/po/templates/fluent.pot b/luci-theme-fluent/po/templates/fluent.pot new file mode 100644 index 000000000..55fbad8ea --- /dev/null +++ b/luci-theme-fluent/po/templates/fluent.pot @@ -0,0 +1,697 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:35+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \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" + +msgid "%d second-level menus" +msgstr "" + +msgid "A new version is available!" +msgstr "" + +msgid "About" +msgstr "" + +msgid "Above bar, end" +msgstr "" + +msgid "Above bar, start" +msgstr "" + +msgid "Accent color" +msgstr "" + +msgid "Add primary menu" +msgstr "" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" + +msgid "Advanced" +msgstr "" + +msgid "Advanced MAC Selector" +msgstr "" + +msgid "and %d more..." +msgstr "" + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" + +msgid "Animation" +msgstr "" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" + +msgid "Author" +msgstr "" + +msgid "Auto-scroll" +msgstr "" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "" + +msgid "Backdrop blur radius" +msgstr "" + +msgid "Backend (GHProxy Acceleration)" +msgstr "" + +msgid "Backend (Official GitHub)" +msgstr "" + +msgid "Base font size" +msgstr "" + +msgid "Below bar, end" +msgstr "" + +msgid "Below bar, start" +msgstr "" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" + +msgid "Cancel" +msgstr "" + +msgid "Card background" +msgstr "" + +msgid "Card shadow" +msgstr "" + +msgid "Check for updates" +msgstr "" + +msgid "Checking for updates..." +msgstr "" + +msgid "Choose color" +msgstr "" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" + +msgid "Clean up" +msgstr "" + +msgid "Clean up invalid items" +msgstr "" + +msgid "Clean up invalid items (%d)" +msgstr "" + +msgid "Close" +msgstr "" + +msgid "Color mode" +msgstr "" + +msgid "Colors" +msgstr "" + +msgid "Comfortable (42px)" +msgstr "" + +msgid "Compact (32px)" +msgstr "" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" + +msgid "Confirm" +msgstr "" + +msgid "Confirm Installation" +msgstr "" + +msgid "Continue" +msgstr "" + +msgid "Control height" +msgstr "" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" + +msgid "Copied" +msgstr "" + +msgid "Copy log" +msgstr "" + +msgid "Corner radius" +msgstr "" + +msgid "Current / Pending MAC Address" +msgstr "" + +msgid "Custom CSS" +msgstr "" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" + +msgid "Dark mode" +msgstr "" + +msgid "Delete primary menu" +msgstr "" + +msgid "Disabled" +msgstr "" + +msgid "Disconnects" +msgstr "" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" + +msgid "Download and Install" +msgstr "" + +msgid "Download log" +msgstr "" + +msgid "Downloaded" +msgstr "" + +msgid "Downloading on router" +msgstr "" + +msgid "Drag to move second-level menu" +msgstr "" + +msgid "Drag to reorder primary menu" +msgstr "" + +msgid "Drop second-level menus here" +msgstr "" + +msgid "Edit" +msgstr "" + +msgid "Enable page transition animation" +msgstr "" + +msgid "Enable tab underline animation" +msgstr "" + +msgid "Errors" +msgstr "" + +msgid "Expecting: %s" +msgstr "" + +msgid "Extra large (12px)" +msgstr "" + +msgid "Failed to check for updates" +msgstr "" + +msgid "Failed to fetch current theme version." +msgstr "" + +msgid "Fast" +msgstr "" + +msgid "Finished" +msgstr "" + +msgid "Fix primary menu title errors before saving." +msgstr "" + +msgid "Fluent Design 2 theme for LuCI" +msgstr "" + +msgid "Fluent Theme" +msgstr "" + +msgid "Fluent theme settings" +msgstr "" + +msgid "Follow system" +msgstr "" + +msgid "Force dark mode" +msgstr "" + +msgid "Force light mode" +msgstr "" + +msgid "Force LTR" +msgstr "" + +msgid "Force RTL" +msgstr "" + +msgid "General" +msgstr "" + +msgid "GitHub Token Required" +msgstr "" + +msgid "Header height" +msgstr "" + +msgid "Height of the top header bar in pixels." +msgstr "" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" + +msgid "HEX color used for container/card elements in dark mode." +msgstr "" + +msgid "HEX color used for container/card elements in light mode." +msgstr "" + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" + +msgid "HEX color used for the main page background in dark mode." +msgstr "" + +msgid "HEX color used for the main page background in light mode." +msgstr "" + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "" + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "" + +msgid "Installation cancelled by user." +msgstr "" + +msgid "Installed version" +msgstr "" + +msgid "Installing package" +msgstr "" + +msgid "Keep action bar visible while scrolling" +msgstr "" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" + +msgid "Large" +msgstr "" + +msgid "Large (8px)" +msgstr "" + +msgid "Light mode" +msgstr "" + +msgid "Line %d" +msgstr "" + +msgid "Lines" +msgstr "" + +msgid "Login" +msgstr "" + +msgid "Login card opacity" +msgstr "" + +msgid "Login page" +msgstr "" + +msgid "Medium" +msgstr "" + +msgid "Medium (4px)" +msgstr "" + +msgid "Menu" +msgstr "" + +msgid "Menu title cannot be empty." +msgstr "" + +msgid "Navigation font weight" +msgstr "" + +msgid "New primary menu" +msgstr "" + +msgid "New primary menu %d" +msgstr "" + +msgid "Next" +msgstr "" + +msgid "Nightly build available (reinstallation check)." +msgstr "" + +msgid "Nightly Channel (Prerelease)" +msgstr "" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" + +msgid "None" +msgstr "" + +msgid "Normal" +msgstr "" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" + +msgid "Package format" +msgstr "" + +msgid "Page background" +msgstr "" + +msgid "Please enter your password." +msgstr "" + +msgid "Please enter your username." +msgstr "" + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" + +msgid "Preset Vendor / Generator" +msgstr "" + +msgid "Primary menu title" +msgstr "" + +msgid "Primary menu titles cannot be empty." +msgstr "" + +msgid "Primary menu titles must be unique." +msgstr "" + +msgid "Progress bar text color" +msgstr "" + +msgid "Progress bar text position" +msgstr "" + +msgid "Random Local MAC" +msgstr "" + +msgid "Randomize MAC" +msgstr "" + +msgid "Reload Web Interface" +msgstr "" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" + +msgid "Respect reduced-motion preference" +msgstr "" + +msgid "Restore %s to its original menu name" +msgstr "" + +msgid "Restore %s to its original menu position" +msgstr "" + +msgid "Restore defaults" +msgstr "" + +msgid "Restore to original title" +msgstr "" + +msgid "Retrying with token..." +msgstr "" + +msgid "Rounded (6px)" +msgstr "" + +msgid "Save" +msgstr "" + +msgid "Scroll to bottom" +msgstr "" + +msgid "Scroll to top" +msgstr "" + +msgid "Search menu items" +msgstr "" + +msgid "Search menu..." +msgstr "" + +msgid "Second-level menu title" +msgstr "" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "" + +msgid "Select vendor prefix..." +msgstr "" + +msgid "Semibold" +msgstr "" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" + +msgid "Show %s in the menu" +msgstr "" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" + +msgid "Show top loading bar" +msgstr "" + +msgid "Show UCI changes while applying configuration" +msgstr "" + +msgid "Sidebar background" +msgstr "" + +msgid "Sidebar width" +msgstr "" + +msgid "Slow" +msgstr "" + +msgid "Small" +msgstr "" + +msgid "Small (2px)" +msgstr "" + +msgid "Software Update" +msgstr "" + +msgid "Source code" +msgstr "" + +msgid "Square (0px)" +msgstr "" + +msgid "Stable Channel" +msgstr "" + +msgid "Starting backend download..." +msgstr "" + +msgid "Starting update process..." +msgstr "" + +msgid "Submit" +msgstr "" + +msgid "Switch theme mode" +msgstr "" + +msgid "Text direction" +msgstr "" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" + +msgid "Theme settings" +msgstr "" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" + +msgid "Theme transition speed" +msgstr "" + +msgid "Toggle fullscreen" +msgstr "" + +msgid "Triggering installation on router..." +msgstr "" + +msgid "Update channel" +msgstr "" + +msgid "Update failed" +msgstr "" + +msgid "Upgrade Successful!" +msgstr "" + +msgid "Use Fluent custom select dropdowns" +msgstr "" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" + +msgid "Username is required." +msgstr "" + +msgid "valid HEX color value" +msgstr "" + +msgid "Warnings" +msgstr "" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" + +msgid "Width of the main navigation sidebar in pixels." +msgstr "" + +msgid "Word wrap" +msgstr "" + +msgid "You can create a new token at" +msgstr "" + +msgid "Your theme is up to date!" +msgstr "" diff --git a/luci-theme-fluent/po/tr/fluent.po b/luci-theme-fluent/po/tr/fluent.po new file mode 100644 index 000000000..b69aedd89 --- /dev/null +++ b/luci-theme-fluent/po/tr/fluent.po @@ -0,0 +1,771 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: tr\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" + +msgid "%d second-level menus" +msgstr "%d alt seviye menü" + +msgid "A new version is available!" +msgstr "Yeni bir sürüm mevcut!" + +msgid "About" +msgstr "Hakkında" + +msgid "Above bar, end" +msgstr "Çubuk üzerindeki son kısım" + +msgid "Above bar, start" +msgstr "Çubuk üzerindeki başlangıç kısmı" + +msgid "Accent color" +msgstr "Vurgu rengi" + +msgid "Add primary menu" +msgstr "Birincil menü ekle" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Yerleşim, tipografi, geçiş zamanlaması, gölgeler gibi ayarları yapın ve " +"yerleşik kontroller yeterli olmadığında özel CSS değişkenleri veya kurallar " +"ekleyin." + +msgid "Advanced" +msgstr "Gelişmiş" + +msgid "Advanced MAC Selector" +msgstr "Gelişmiş MAC Seçici" + +msgid "and %d more..." +msgstr "ve %d daha fazla..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Yerel LuCI sekmeleri ve temalı sekme menüleri arasında geçiş yaparken, etkin " +"alt çizginin animasyonunu sağlayın." + +msgid "Animation" +msgstr "Animasyon" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"API hız sınırı aşıldı. Devam etmek için bir GitHub Token girin. Bu token " +"yalnızca ön uç istekleri için kullanılır ve yönlendirici arka ucunda " +"kaydedilmez." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Standart düğmeler, giriş alanları, seçim kutuları ve tema genelindeki benzer " +"form kontrollerine uygulanır." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"%d geçersiz/yüklü olmayan menü öğesini veya kuralı temizlemek istediğinizden " +"emin misiniz?" + +msgid "Author" +msgstr "Yazar" + +msgid "Auto-scroll" +msgstr "Otomatik kaydırma" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Otomatik (yalnızca Arapça/Farsça yerel ayarlar)" + +msgid "Backdrop blur radius" +msgstr "Arka plan bulanıklık yarıçapı" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Arka Uç (GHProxy Hızlandırma)" + +msgid "Backend (Official GitHub)" +msgstr "Arka Uç (Resmi GitHub)" + +msgid "Base font size" +msgstr "Temel yazı boyutu" + +msgid "Below bar, end" +msgstr "Çubuğun altındaki son kısım" + +msgid "Below bar, start" +msgstr "Çubuğun altındaki başlangıç kısmı" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Karanlık modda, giriş kartının arkasındaki bulanıklık yarıçapı (piksel). " +"Bulanıklığı devre dışı bırakmak için 0 kullanın." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Açık modda, giriş kartının arkasındaki bulanıklık yarıçapı (piksel). " +"Bulanıklığı devre dışı bırakmak için 0 kullanın." + +msgid "Cancel" +msgstr "İptal" + +msgid "Card background" +msgstr "Kart arka planı" + +msgid "Card shadow" +msgstr "Kart gölgelendirmesi" + +msgid "Check for updates" +msgstr "Güncellemeleri kontrol et" + +msgid "Checking for updates..." +msgstr "Güncellemeler aranıyor..." + +msgid "Choose color" +msgstr "Renk seç" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Kimlik doğrulama ve giriş sayfaları için belge yönünü seçin. Otomatik mod, " +"Arapça ve Farsça dil kodlarını (ar, ar_*, ar-*, fa, fa_*, fa-*) sağdan sola " +"(RTL) olarak çözer ve eksik veya tanınmayan yerel ayarlar için soldan sağa " +"(LTR) olarak geri döner." + +msgid "Clean up" +msgstr "Temizle" + +msgid "Clean up invalid items" +msgstr "Geçersiz öğeleri temizle" + +msgid "Clean up invalid items (%d)" +msgstr "Geçersiz öğeleri temizle (%d)" + +msgid "Close" +msgstr "Kapat" + +msgid "Color mode" +msgstr "Renk modu" + +msgid "Colors" +msgstr "Renkler" + +msgid "Comfortable (42px)" +msgstr "Rahat (42px)" + +msgid "Compact (32px)" +msgstr "Kompakt (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Renk modunu, vurgu renklerini, düzen boyutlandırmasını, animasyon " +"davranışını, giriş sayfası görünümünü ve luci-theme-fluent için gelişmiş CSS " +"geçersiz kılmalarını yapılandırın." + +msgid "Confirm" +msgstr "Onayla" + +msgid "Confirm Installation" +msgstr "Yüklemeyi Onayla" + +msgid "Continue" +msgstr "Devam Et" + +msgid "Control height" +msgstr "Denetim yüksekliği" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Ana gezinme etiketleri ve ilgili tema metin vurguları tarafından kullanılan " +"yazı tipi kalınlığını kontrol eder." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Kartlar, düğmeler, giriş alanları ve ilgili kullanıcı arayüzü öğeleri " +"tarafından paylaşılan Fluent köşe yarıçapı belirteçlerini kontrol eder." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Menü, başlık ve diğer tema animasyonları tarafından paylaşılan Fluent geçiş " +"zamanlamasını kontrol eder." + +msgid "Copied" +msgstr "Kopyalandı" + +msgid "Copy log" +msgstr "Günlük kaydı kopyala" + +msgid "Corner radius" +msgstr "Köşe yarıçapı" + +msgid "Current / Pending MAC Address" +msgstr "Mevcut / Bekleyen MAC Adresi" + +msgid "Custom CSS" +msgstr "Özel CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Giriş sayfası arka planını, kart opaklığını ve açık ve karanlık modlar için " +"bulanıklık yarıçapını özelleştirin." + +msgid "Dark mode" +msgstr "Karanlık mod" + +msgid "Delete primary menu" +msgstr "Birincil menüyü sil" + +msgid "Disabled" +msgstr "Devre Dışı" + +msgid "Disconnects" +msgstr "Ayrılanlar" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Sayfa yüklenmeleri ve geçişler sırasında temalı yükleme göstergesini en üst " +"kenarda görüntüle." + +msgid "Download and Install" +msgstr "İndir ve Yükle" + +msgid "Download log" +msgstr "Günlük kaydını indir" + +msgid "Downloaded" +msgstr "İndirildi" + +msgid "Downloading on router" +msgstr "Router'da indirme yapılıyor" + +msgid "Drag to move second-level menu" +msgstr "Alt seviye menüyü taşımak için sürükleyin" + +msgid "Drag to reorder primary menu" +msgstr "Birincil menüyü yeniden sıralamak için sürükleyin" + +msgid "Drop second-level menus here" +msgstr "Alt seviye menüleri buraya bırakın" + +msgid "Edit" +msgstr "Düzenle" + +msgid "Enable page transition animation" +msgstr "Sayfa geçiş animasyonunu etkinleştir" + +msgid "Enable tab underline animation" +msgstr "Sekme alt çizgi animasyonunu etkinleştir" + +msgid "Errors" +msgstr "Hatalar" + +msgid "Expecting: %s" +msgstr "Bekleniyor: %s" + +msgid "Extra large (12px)" +msgstr "Çok büyük (12px)" + +msgid "Failed to check for updates" +msgstr "Güncelleme kontrolü başarısız oldu" + +msgid "Failed to fetch current theme version." +msgstr "Mevcut tema sürümü alınamadı." + +msgid "Fast" +msgstr "Hızlı" + +msgid "Finished" +msgstr "Tamamlandı" + +msgid "Fix primary menu title errors before saving." +msgstr "Kaydetmeden önce birincil menü başlığındaki hataları düzeltin." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "LuCI için Fluent Design 2 teması" + +msgid "Fluent Theme" +msgstr "Fluent Teması" + +msgid "Fluent theme settings" +msgstr "Fluent tema ayarları" + +msgid "Follow system" +msgstr "Sistem ayarlarını uygula" + +msgid "Force dark mode" +msgstr "Karanlık modu zorla etkinleştir" + +msgid "Force light mode" +msgstr "Açık modu zorla etkinleştir" + +msgid "Force LTR" +msgstr "Soldan sağa yönlendirmeyi (LTR) zorla kullan" + +msgid "Force RTL" +msgstr "Sağdan sola yönlendirmeyi (RTL) zorla kullan" + +msgid "General" +msgstr "Genel" + +msgid "GitHub Token Required" +msgstr "GitHub belirteci gereklidir" + +msgid "Header height" +msgstr "Üst başlık yüksekliği" + +msgid "Height of the top header bar in pixels." +msgstr "En üstteki başlık çubuğunun piksel cinsinden yüksekliği." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Arayüz karanlık modda görüntülendiğinde kullanılan birincil Fluent vurgu " +"rengi (HEX)." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Arayüz açık modda görüntülendiğinde kullanılan birincil Fluent vurgu rengi " +"(HEX)." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "Karanlık modda kullanılan konteyner/kart elemanları için HEX renk." + +msgid "HEX color used for container/card elements in light mode." +msgstr "Açık modda kullanılan konteyner/kart elemanları için HEX renk." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"Arayüz karanlık modda görüntülendiğinde kullanılan ilerleme çubuğu " +"etiketleri için HEX renk." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Arayüz açık modda görüntülendiğinde kullanılan ilerleme çubuğu etiketleri " +"için HEX renk." + +msgid "HEX color used for the main page background in dark mode." +msgstr "Karanlık modda ana sayfa arka planı için kullanılan HEX renk." + +msgid "HEX color used for the main page background in light mode." +msgstr "Açık modda ana sayfa arka planı için kullanılan HEX renk." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "Karanlık modda navigasyon yan çubuğu için kullanılan HEX rengi." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "Açık modda navigasyon yan çubuğu için kullanılan HEX rengi." + +msgid "Installation cancelled by user." +msgstr "Kurulum kullanıcı tarafından iptal edildi." + +msgid "Installed version" +msgstr "Yüklenen sürüm" + +msgid "Installing package" +msgstr "Paket yükleniyor" + +msgid "Keep action bar visible while scrolling" +msgstr "Kaydırma sırasında işlem çubuğunu görünür tutun" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Kaydırma sırasında Kaydet, Uygula ve Sıfırla işlemlerini, ekranın alt " +"kısmında sabit tutun." + +msgid "Large" +msgstr "Büyük" + +msgid "Large (8px)" +msgstr "Büyük (8px)" + +msgid "Light mode" +msgstr "Açık mod" + +msgid "Line %d" +msgstr "%d. satır" + +msgid "Lines" +msgstr "Satırlar" + +msgid "Login" +msgstr "Giriş" + +msgid "Login card opacity" +msgstr "Giriş kartı opaklığı" + +msgid "Login page" +msgstr "Giriş sayfası" + +msgid "Medium" +msgstr "Orta" + +msgid "Medium (4px)" +msgstr "Orta (4px)" + +msgid "Menu" +msgstr "Menü" + +msgid "Menu title cannot be empty." +msgstr "Menü başlığı boş olamaz." + +msgid "Navigation font weight" +msgstr "Navigasyon yazı tipi kalınlığı" + +msgid "New primary menu" +msgstr "Yeni birincil menü" + +msgid "New primary menu %d" +msgstr "Yeni birincil menü %d" + +msgid "Next" +msgstr "İleri" + +msgid "Nightly build available (reinstallation check)." +msgstr "Nightly derlemesi mevcut (yeniden yükleme kontrolü)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Nightly Kanalı (Ön Sürüm)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "Bu sürümde sistem mimariniz için uygun paket dosyası bulunamadı." + +msgid "None" +msgstr "Yok" + +msgid "Normal" +msgstr "Normal" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Karanlık modda giriş kartının opaklığı. 0 tamamen şeffaf, 1 ise tamamen opak " +"anlamına gelir." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Açık modda giriş kartının opaklığı. 0 tamamen şeffaf, 1 ise tamamen opak " +"anlamına gelir." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Fluent başlık şablonuna eklenen ham CSS. Ek --fluent-* değişkeni geçersiz " +"kılmaları veya özel seçenekler olarak sunulmayan sayfa özelindeki " +"ayarlamalar için kullanın." + +msgid "Package format" +msgstr "Paket formatı" + +msgid "Page background" +msgstr "Sayfa arka planı" + +msgid "Please enter your password." +msgstr "Lütfen şifrenizi girin." + +msgid "Please enter your username." +msgstr "Lütfen kullanıcı adınızı girin." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"İlerleme çubuğu etiketlerinin, çubukla ilişkili konumu. Başlangıç/bitiş " +"hizalaması metin yönüne (SOL-SAĞ veya SAĞ-SOL) göre belirlenir." + +msgid "Preset Vendor / Generator" +msgstr "Önceden Ayarlanmış Satıcı / Oluşturucu" + +msgid "Primary menu title" +msgstr "Ana menü başlığı" + +msgid "Primary menu titles cannot be empty." +msgstr "Ana menü başlıkları boş olamaz." + +msgid "Primary menu titles must be unique." +msgstr "Ana menü başlıkları benzersiz olmalıdır." + +msgid "Progress bar text color" +msgstr "İlerleme çubuğu metin rengi" + +msgid "Progress bar text position" +msgstr "İlerleme çubuğu metni konumu" + +msgid "Random Local MAC" +msgstr "Rastgele Yerel MAC Adresi" + +msgid "Randomize MAC" +msgstr "MAC Adresini Rastgeleleştir" + +msgid "Reload Web Interface" +msgstr "Web Arayüzünü Yeniden Yükle" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "Yerleşik seçim öğelerini, temanın özel açılır menü bileşeniyle değiştir." + +msgid "Respect reduced-motion preference" +msgstr "Azaltılmış hareket tercihine saygı göster" + +msgid "Restore %s to its original menu name" +msgstr "%s'i orijinal menü adına geri yükle" + +msgid "Restore %s to its original menu position" +msgstr "%s'i orijinal menü konumuna geri yükle" + +msgid "Restore defaults" +msgstr "Varsayılanlara geri yükle" + +msgid "Restore to original title" +msgstr "Orijinal başlığa geri yükle" + +msgid "Retrying with token..." +msgstr "Token ile yeniden deniyor..." + +msgid "Rounded (6px)" +msgstr "Yuvarlak (6px)" + +msgid "Save" +msgstr "Kaydet" + +msgid "Scroll to bottom" +msgstr "Aşağıya kaydır" + +msgid "Scroll to top" +msgstr "Yukarı kaydır" + +msgid "Search menu items" +msgstr "Menü öğelerini ara" + +msgid "Search menu..." +msgstr "Menüyü ara..." + +msgid "Second-level menu title" +msgstr "İkinci seviye menü başlığı" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "Temalı kartlara ve panellere uygulanan gölge derinliğini seçin." + +msgid "Select vendor prefix..." +msgstr "Satıcı önekini seç..." + +msgid "Semibold" +msgstr "Yarı Kalın" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Arayüzün temel yazı tipi boyutunu piksel cinsinden ayarlar. Çoğu tema metni, " +"Fluent CSS değişkenleri aracılığıyla bu değerden ölçeklenir. Önerilen " +"aralık: 12-18px." + +msgid "Show %s in the menu" +msgstr "%s'i menüde göster" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Yapılandırma uygulanana kadar, üst kısımda bekleyen UCI değişikliklerini " +"gösterin." + +msgid "Show top loading bar" +msgstr "Üst yükleme çubuğunu göster" + +msgid "Show UCI changes while applying configuration" +msgstr "Yapılandırma uygulanırken UCI değişikliklerini göster." + +msgid "Sidebar background" +msgstr "Yan çubuk arka planı" + +msgid "Sidebar width" +msgstr "Yan çubuk genişliği" + +msgid "Slow" +msgstr "Yavaş" + +msgid "Small" +msgstr "Küçük" + +msgid "Small (2px)" +msgstr "Küçük (2px)" + +msgid "Software Update" +msgstr "Yazılım Güncellemesi" + +msgid "Source code" +msgstr "Kaynak kodu" + +msgid "Square (0px)" +msgstr "Kare (0px)" + +msgid "Stable Channel" +msgstr "Kararlı Kanal" + +msgid "Starting backend download..." +msgstr "Arka uç indirme başlatılıyor..." + +msgid "Starting update process..." +msgstr "Güncelleme işlemi başlatılıyor..." + +msgid "Submit" +msgstr "Gönder" + +msgid "Switch theme mode" +msgstr "Tema modunu değiştir" + +msgid "Text direction" +msgstr "Metin yönü" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"Kaydedilen menü düzeni geçersiz. \"Varsayılanlara geri yükle\" seçeneğini " +"kullanın veya kaydetmeden önce bir düzenleme yapın." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"Kaydedilen menü düzeni geçersiz. Varsayılanlara geri yükleyin veya " +"kaydetmeden önce düzeni düzenleyin." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Tema, yönlendiricinize kuruluyor. Günlükler aşağıda görüntüleniyor. Web " +"arayüzünü yeniden yüklemek, RPCD yeniden başlatıldığında değişiklikleri " +"uygulayacaktır." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "Tema paketi hazır. Kurulumu gerçekten devam etmek istiyorsunuz?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Bu token için herhangi bir izin/erişim yetkisi verilmesine gerek YOK (sadece " +"okuma erişimi yeterlidir)." + +msgid "Theme settings" +msgstr "Tema ayarları" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Tema başarıyla güncellendi! RPC hizmeti yeniden yükleniyor, lütfen sayfayı 5 " +"saniye içinde yenileyin." + +msgid "Theme transition speed" +msgstr "Tema geçiş hızı" + +msgid "Toggle fullscreen" +msgstr "Tam ekranı aç/kapat" + +msgid "Triggering installation on router..." +msgstr "Kurulum yönlendiricide başlatılıyor..." + +msgid "Update channel" +msgstr "Güncelleme kanalı" + +msgid "Update failed" +msgstr "Güncelleme başarısız oldu" + +msgid "Upgrade Successful!" +msgstr "Yükseltme başarılı!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Fluent özel açılır menüleri kullan" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"LuCI sayfaları arasında gezinirken animasyonlar için tarayıcı View " +"Transition API'sini kullanın (destekleniyorsa)." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Sistem/tarayıcı tercihini kullanın veya Fluent temasını her zaman sabit bir " +"açık veya karanlık paletle görüntüleyin." + +msgid "Username is required." +msgstr "Kullanıcı adı gereklidir." + +msgid "valid HEX color value" +msgstr "Geçerli HEX renk değeri" + +msgid "Warnings" +msgstr "Uyarılar" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Etkinleştirildiğinde, Fluent animasyonları tarayıcı veya işletim sisteminin " +"hareket azaltma tercihini takip eder." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Ana gezinme yan çubuğunun genişliği (piksel cinsinden)." + +msgid "Word wrap" +msgstr "Metin kaydırma" + +msgid "You can create a new token at" +msgstr "Yeni bir belirteç oluşturabileceğiniz adres:" + +msgid "Your theme is up to date!" +msgstr "Temanız güncel." diff --git a/luci-theme-fluent/po/uk/fluent.po b/luci-theme-fluent/po/uk/fluent.po new file mode 100644 index 000000000..b4e2a9c4e --- /dev/null +++ b/luci-theme-fluent/po/uk/fluent.po @@ -0,0 +1,773 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: uk\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" + +msgid "%d second-level menus" +msgstr "%d другорядні меню" + +msgid "A new version is available!" +msgstr "Доступна нова версія!" + +msgid "About" +msgstr "Про" + +msgid "Above bar, end" +msgstr "Кінець над смужкою" + +msgid "Above bar, start" +msgstr "Початок над смужкою" + +msgid "Accent color" +msgstr "Акцентний колір" + +msgid "Add primary menu" +msgstr "Додати головне меню" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Налаштуйте макет, типографіку, час переходу, тіні та додавайте власні " +"CSS-змінні або правила, якщо вбудованих елементів керування недостатньо." + +msgid "Advanced" +msgstr "Розширені" + +msgid "Advanced MAC Selector" +msgstr "Розширений вибір MAC-адрес" + +msgid "and %d more..." +msgstr "і ще %d..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Анімуйте активну підкреслену лінію при перемиканні між стандартними " +"вкладками LuCI та тематичними меню." + +msgid "Animation" +msgstr "Анімація" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Досягнуто обмеження швидкості API. Будь ласка, введіть токен GitHub, щоб " +"продовжити. Цей токен використовується лише для запитів інтерфейсу та не " +"зберігається на сервері маршрутизатора." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Застосовується до стандартних кнопок, полів введення, випадаючих списків та " +"інших елементів керування формами в темі." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Ви впевнені, що хочете видалити %d недійсних/неінстальованих пунктів меню " +"або правил?" + +msgid "Author" +msgstr "Автор" + +msgid "Auto-scroll" +msgstr "Автоматична прокрутка" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Автоматично (лише для локалей арабською/перською мовами)" + +msgid "Backdrop blur radius" +msgstr "Радіус розмиття фону" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Серверна частина (прискорення через GHProxy)" + +msgid "Backend (Official GitHub)" +msgstr "Backend (Офіційний GitHub)" + +msgid "Base font size" +msgstr "Розмір базового шрифту" + +msgid "Below bar, end" +msgstr "Кінець під смужкою" + +msgid "Below bar, start" +msgstr "Початок під смужкою" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Радіус розмиття в пікселях за карткою входу в темному режимі. Використовуйте " +"0, щоб вимкнути розмиття." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Радіус розмиття в пікселях за карткою входу в світлому режимі. " +"Використовуйте 0, щоб вимкнути розмиття." + +msgid "Cancel" +msgstr "Скасувати" + +msgid "Card background" +msgstr "Фон картки" + +msgid "Card shadow" +msgstr "Тінь картки" + +msgid "Check for updates" +msgstr "Перевірити наявність оновлень" + +msgid "Checking for updates..." +msgstr "Перевірка наявності оновлень..." + +msgid "Choose color" +msgstr "Вибрати колір" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Оберіть напрямок тексту для аутентифікованих сторінок та сторінки входу. " +"Автоматичний режим визначає арабські та перські локалі (ar, ar_*, ar-*, fa, " +"fa_*, fa-*) як RTL і автоматично перемикається на LTR для відсутніх або " +"нерозпізнаних локалей." + +msgid "Clean up" +msgstr "Очистити" + +msgid "Clean up invalid items" +msgstr "Очистити недійсні елементи" + +msgid "Clean up invalid items (%d)" +msgstr "Очистити недійсні елементи (%d)" + +msgid "Close" +msgstr "Закрити" + +msgid "Color mode" +msgstr "Колірний режим" + +msgid "Colors" +msgstr "Кольори" + +msgid "Comfortable (42px)" +msgstr "Зручний (42px)" + +msgid "Compact (32px)" +msgstr "Компактний (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Налаштуйте колірний режим, акцентні кольори, розміри макету, поведінку " +"анімації, зовнішній вигляд сторінки входу та розширені CSS-налаштування для " +"luci-theme-fluent." + +msgid "Confirm" +msgstr "Підтвердити" + +msgid "Confirm Installation" +msgstr "Підтвердити встановлення" + +msgid "Continue" +msgstr "Продовжити" + +msgid "Control height" +msgstr "Висота елементів керування" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Встановлює товщину шрифту, що використовується для міток головної навігації " +"та пов'язаних елементів оформлення теми." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Встановлює спільні значення радіуса кутів Fluent, які використовуються для " +"карток, кнопок, полів введення та інших елементів інтерфейсу." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Встановлює спільне значення часу переходу Fluent, яке використовується для " +"меню, заголовка та інших анімацій теми." + +msgid "Copied" +msgstr "Скопіровано" + +msgid "Copy log" +msgstr "Копіювати журнал" + +msgid "Corner radius" +msgstr "Радіус кутів" + +msgid "Current / Pending MAC Address" +msgstr "Поточна / Очікувана MAC-адреса" + +msgid "Custom CSS" +msgstr "Власні CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Налаштуйте фон сторінки входу, непрозорість карток і радіус розмиття для " +"світлого та темного режимів." + +msgid "Dark mode" +msgstr "Темний режим" + +msgid "Delete primary menu" +msgstr "Видалити головне меню" + +msgid "Disabled" +msgstr "Вимкнено" + +msgid "Disconnects" +msgstr "Відключення" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Відображати тематичний індикатор завантаження у верхній частині під час " +"завантаження сторінок та переходів." + +msgid "Download and Install" +msgstr "Завантажити та встановити" + +msgid "Download log" +msgstr "Завантажити журнал" + +msgid "Downloaded" +msgstr "Завантажено" + +msgid "Downloading on router" +msgstr "Завантаження на роутері" + +msgid "Drag to move second-level menu" +msgstr "Перетягніть для переміщення другорядного меню" + +msgid "Drag to reorder primary menu" +msgstr "Перетягніть для зміни порядку головних меню" + +msgid "Drop second-level menus here" +msgstr "Тут можна розмістити другорядні меню" + +msgid "Edit" +msgstr "Редагувати" + +msgid "Enable page transition animation" +msgstr "Увімкнути анімацію переходу між сторінками" + +msgid "Enable tab underline animation" +msgstr "Увімкнути анімацію підкреслення вкладок" + +msgid "Errors" +msgstr "Помилки" + +msgid "Expecting: %s" +msgstr "Очікується: %s" + +msgid "Extra large (12px)" +msgstr "Дуже великий (12px)" + +msgid "Failed to check for updates" +msgstr "Не вдалося перевірити наявність оновлень" + +msgid "Failed to fetch current theme version." +msgstr "Не вдалося отримати поточну версію теми." + +msgid "Fast" +msgstr "Швидко" + +msgid "Finished" +msgstr "Завершено" + +msgid "Fix primary menu title errors before saving." +msgstr "Виправте помилки заголовків головних меню перед збереженням." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Тема Fluent Design 2 для LuCI" + +msgid "Fluent Theme" +msgstr "Тема Fluent" + +msgid "Fluent theme settings" +msgstr "Налаштування теми Fluent" + +msgid "Follow system" +msgstr "Використовувати системні налаштування" + +msgid "Force dark mode" +msgstr "Примусово увімкнути темний режим" + +msgid "Force light mode" +msgstr "Примусово увімкнути світлий режим" + +msgid "Force LTR" +msgstr "Примусово LTR (зліва направо)" + +msgid "Force RTL" +msgstr "Примусово RTL (справа наліво)" + +msgid "General" +msgstr "Загальні налаштування" + +msgid "GitHub Token Required" +msgstr "Потрібен токен GitHub" + +msgid "Header height" +msgstr "Висота заголовка" + +msgid "Height of the top header bar in pixels." +msgstr "Висота верхньої панелі заголовка в пікселях." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Шестнадцятковий код кольору, що використовується як основний акцент Fluent у " +"темному режимі." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Шестнадцятковий код кольору, що використовується як основний акцент Fluent у " +"світлому режимі." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "Шестнадцятковий код кольору для контейнерів/карток у темному режимі." + +msgid "HEX color used for container/card elements in light mode." +msgstr "Шестнадцятковий код кольору для контейнерів/карток у світлому режимі." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "Шестнадцятковий код кольору для міток індикаторів прогресу в темному режимі." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Шестнадцятковий код кольору для міток індикаторів прогресу у світлому " +"режимі." + +msgid "HEX color used for the main page background in dark mode." +msgstr "Шестнадцятковий код кольору для фону головної сторінки в темному режимі." + +msgid "HEX color used for the main page background in light mode." +msgstr "Шестнадцятковий код кольору для фону головної сторінки у світлому режимі." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "" +"Шестнадцятковий код кольору, що використовується для навігаційної бічної " +"панелі в темному режимі." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "" +"Шестнадцятковий код кольору, що використовується для навігаційної бічної " +"панелі у світлому режимі." + +msgid "Installation cancelled by user." +msgstr "Встановлення скасовано користувачем." + +msgid "Installed version" +msgstr "Встановлена версія" + +msgid "Installing package" +msgstr "Встановлення пакета" + +msgid "Keep action bar visible while scrolling" +msgstr "Залишати панель дій видимою під час прокручування." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "" +"Фіксувати кнопки Зберегти, Застосувати та Скинути внизу вікна при " +"прокручуванні." + +msgid "Large" +msgstr "Великий" + +msgid "Large (8px)" +msgstr "Великий (8px)" + +msgid "Light mode" +msgstr "Світлий режим" + +msgid "Line %d" +msgstr "Рядок %d" + +msgid "Lines" +msgstr "Рядки" + +msgid "Login" +msgstr "Увійти" + +msgid "Login card opacity" +msgstr "Непрозорість картки входу" + +msgid "Login page" +msgstr "Сторінка входу" + +msgid "Medium" +msgstr "Середній" + +msgid "Medium (4px)" +msgstr "Середній (4px)" + +msgid "Menu" +msgstr "Меню" + +msgid "Menu title cannot be empty." +msgstr "Назва меню не може бути порожньою." + +msgid "Navigation font weight" +msgstr "Товщина шрифту навігації" + +msgid "New primary menu" +msgstr "Нове головне меню" + +msgid "New primary menu %d" +msgstr "Нове головне меню %d" + +msgid "Next" +msgstr "Далі" + +msgid "Nightly build available (reinstallation check)." +msgstr "Доступна збірка Nightly (перевірка потреби перевстановлення)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Канал Nightly (попередній випуск)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" +"Не знайдено відповідного файлу пакета для вашої архітектури системи в цій " +"версії." + +msgid "None" +msgstr "Відсутнє" + +msgid "Normal" +msgstr "Звичайний" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Непрозорість картки входу в темному режимі. 0 – повністю прозора, 1 – " +"повністю непрозора." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Непрозорість картки входу в світлому режимі. 0 – повністю прозора, 1 – " +"повністю непрозора." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"Додатковий CSS-код, який вставляється у шаблон заголовка Fluent. " +"Використовуйте це для додаткових перевизначень змінних --fluent-* або для " +"внесення змін на конкретній сторінці, які не доступні як окремі параметри." + +msgid "Package format" +msgstr "Формат пакета" + +msgid "Page background" +msgstr "Фон сторінки" + +msgid "Please enter your password." +msgstr "Будь ласка, введіть ваш пароль." + +msgid "Please enter your username." +msgstr "Будь ласка, введіть ваше ім'я користувача." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Розташування міток прогрес-бару відносно смужки. Вирівнювання початку/кінця " +"відповідає напрямку тексту (зліва направо або справа наліво)." + +msgid "Preset Vendor / Generator" +msgstr "Попередньо заданий виробник / генератор" + +msgid "Primary menu title" +msgstr "Заголовок основного меню" + +msgid "Primary menu titles cannot be empty." +msgstr "Заголовки основного меню не можуть бути порожніми." + +msgid "Primary menu titles must be unique." +msgstr "Заголовки основного меню повинні бути унікальними." + +msgid "Progress bar text color" +msgstr "Колір тексту індикатора прогресу" + +msgid "Progress bar text position" +msgstr "Позиція тексту прогрес-бару" + +msgid "Random Local MAC" +msgstr "Випадкова локальна MAC-адреса" + +msgid "Randomize MAC" +msgstr "Випадково змінити MAC-адресу" + +msgid "Reload Web Interface" +msgstr "Перезавантажити веб-інтерфейс" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "Замінити стандартні елементи вибору на спеціальний випадаючий список теми." + +msgid "Respect reduced-motion preference" +msgstr "Враховувати налаштування зменшення анімації." + +msgid "Restore %s to its original menu name" +msgstr "Відновити назву меню \"%s\" до початкової" + +msgid "Restore %s to its original menu position" +msgstr "Відновити позицію меню \"%s\" до початкової" + +msgid "Restore defaults" +msgstr "Відновити налаштування за замовчуванням" + +msgid "Restore to original title" +msgstr "Відновити оригінальну назву" + +msgid "Retrying with token..." +msgstr "Повторний запуск з токеном..." + +msgid "Rounded (6px)" +msgstr "З округленням (6px)" + +msgid "Save" +msgstr "Зберегти" + +msgid "Scroll to bottom" +msgstr "Прокрутити до низу" + +msgid "Scroll to top" +msgstr "Прокрутка до верху" + +msgid "Search menu items" +msgstr "Пошук по пунктах меню" + +msgid "Search menu..." +msgstr "Пошук у меню..." + +msgid "Second-level menu title" +msgstr "Назва підменю" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "Виберіть глибину тіні, що застосовується до карток і панелей теми." + +msgid "Select vendor prefix..." +msgstr "Вибрати префікс постачальника..." + +msgid "Semibold" +msgstr "Напівжирний" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Встановлює базовий розмір шрифту інтерфейсу в пікселях. Більшість тексту " +"теми масштабується від цього значення за допомогою CSS-змінних Fluent. " +"Рекомендований діапазон: 12-18px." + +msgid "Show %s in the menu" +msgstr "Показати \"%s\" у меню" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Показувати очікувані зміни конфігурації в повідомленні про стан застосування " +"у верхній частині, доки не завершиться застосування." + +msgid "Show top loading bar" +msgstr "Показати верхню смугу завантаження" + +msgid "Show UCI changes while applying configuration" +msgstr "Показувати зміни конфігурації під час її застосування." + +msgid "Sidebar background" +msgstr "Фон бічної панелі" + +msgid "Sidebar width" +msgstr "Ширина бічної панелі" + +msgid "Slow" +msgstr "Повільно" + +msgid "Small" +msgstr "Малий" + +msgid "Small (2px)" +msgstr "Малий (2 пікселі)" + +msgid "Software Update" +msgstr "Оновлення програмного забезпечення" + +msgid "Source code" +msgstr "Вихідний код" + +msgid "Square (0px)" +msgstr "Квадрат (0 пікселів)" + +msgid "Stable Channel" +msgstr "Стабільний канал" + +msgid "Starting backend download..." +msgstr "Початок завантаження бекенду..." + +msgid "Starting update process..." +msgstr "Початок процесу оновлення..." + +msgid "Submit" +msgstr "Надіслати" + +msgid "Switch theme mode" +msgstr "Змінити тему" + +msgid "Text direction" +msgstr "Напрямок тексту" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"Збережене розташування меню недійсне. Оберіть \"Відновити налаштування за " +"замовчуванням\" або внесіть зміни перед збереженням." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"Збережене розташування меню недійсне. Відновіть налаштування за " +"замовчуванням або відредагуйте розташування перед збереженням." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Тема встановлюється на вашому роутері. Журнали відображаються нижче. " +"Перезавантаження веб-інтерфейсу застосує зміни після перезапуску RPCD." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "Пакет теми готовий. Ви впевнені, що хочете продовжити встановлення?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Цей токен НЕ потребує жодних дозволів/областей (достатньо публічного доступу " +"лише для читання)." + +msgid "Theme settings" +msgstr "Налаштування теми" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Тема успішно оновлена! Перезавантаження RPC-сервісу, будь ласка, оновіть " +"сторінку через 5 секунд." + +msgid "Theme transition speed" +msgstr "Швидкість переходів теми" + +msgid "Toggle fullscreen" +msgstr "Перемкнути повноекранний режим" + +msgid "Triggering installation on router..." +msgstr "Запуск встановлення на роутері..." + +msgid "Update channel" +msgstr "Канал оновлень" + +msgid "Update failed" +msgstr "Оновлення не вдалося" + +msgid "Upgrade Successful!" +msgstr "Оновлення успішно завершено!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Використовувати спеціальні випадаючі списки Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Використовуйте View Transition API браузера для анімації навігації між " +"сторінками LuCI, якщо це підтримується." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Використовуйте системні/налаштування браузера або завжди відображайте тему " +"Fluent у фіксованій світлій або темній колірній схемі." + +msgid "Username is required." +msgstr "Ім'я користувача обов'язкове." + +msgid "valid HEX color value" +msgstr "дійсне значення кольору в HEX" + +msgid "Warnings" +msgstr "Попередження" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Якщо увімкнено, анімація Fluent враховує налаштування зменшення руху " +"браузера або операційної системи." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Ширина основної навігаційної бічної панелі в пікселях." + +msgid "Word wrap" +msgstr "Перенос слів" + +msgid "You can create a new token at" +msgstr "Ви можете створити новий токен за адресою:" + +msgid "Your theme is up to date!" +msgstr "Ваша тема актуальна!" diff --git a/luci-theme-fluent/po/vi/fluent.po b/luci-theme-fluent/po/vi/fluent.po new file mode 100644 index 000000000..3968a720b --- /dev/null +++ b/luci-theme-fluent/po/vi/fluent.po @@ -0,0 +1,770 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:38+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\n" +"Language: vi\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" + +msgid "%d second-level menus" +msgstr "%d mục phụ" + +msgid "A new version is available!" +msgstr "Một phiên bản mới đã có!" + +msgid "About" +msgstr "Giới thiệu" + +msgid "Above bar, end" +msgstr "Cuối thanh." + +msgid "Above bar, start" +msgstr "Đầu thanh." + +msgid "Accent color" +msgstr "Màu điểm nhấn" + +msgid "Add primary menu" +msgstr "Thêm mục chính" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "" +"Điều chỉnh bố cục, kiểu chữ, thời gian chuyển đổi, đổ bóng, và thêm các biến " +"CSS hoặc quy tắc tùy chỉnh khi các điều khiển tích hợp không đủ." + +msgid "Advanced" +msgstr "Nâng cao" + +msgid "Advanced MAC Selector" +msgstr "Bộ chọn MAC nâng cao" + +msgid "and %d more..." +msgstr "và %d mục khác..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "" +"Hoạt ảnh đường gạch chân khi chuyển đổi giữa các tab LuCI gốc và menu tab " +"theo chủ đề." + +msgid "Animation" +msgstr "Hoạt ảnh" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "" +"Vượt quá giới hạn tốc độ API. Vui lòng nhập một Token GitHub để tiếp tục. " +"Token này chỉ được sử dụng cho các yêu cầu giao diện người dùng và sẽ không " +"được lưu trên hệ thống của router." + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "" +"Áp dụng cho các nút, ô nhập liệu, trình chọn và các điều khiển biểu mẫu " +"tương tự trong toàn bộ chủ đề." + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "" +"Bạn có chắc chắn muốn xóa %d mục hoặc quy tắc không hợp lệ/không được cài " +"đặt?" + +msgid "Author" +msgstr "Tác giả" + +msgid "Auto-scroll" +msgstr "Tự động cuộn" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "Tự động (chỉ dành cho ngôn ngữ Ả Rập/Ba Tư)" + +msgid "Backdrop blur radius" +msgstr "Bán kính làm mờ nền" + +msgid "Backend (GHProxy Acceleration)" +msgstr "Hệ thống phía sau (GHProxy Acceleration)" + +msgid "Backend (Official GitHub)" +msgstr "Phần nền (GitHub chính thức)" + +msgid "Base font size" +msgstr "Kích thước phông chữ cơ bản" + +msgid "Below bar, end" +msgstr "Kết thúc dưới thanh." + +msgid "Below bar, start" +msgstr "Bắt đầu dưới thanh." + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "" +"Bán kính làm mờ (pixel) phía sau thẻ đăng nhập ở chế độ tối. Sử dụng 0 để " +"tắt tính năng làm mờ." + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "" +"Bán kính làm mờ (pixel) phía sau thẻ đăng nhập ở chế độ sáng. Sử dụng 0 để " +"tắt tính năng làm mờ." + +msgid "Cancel" +msgstr "Hủy" + +msgid "Card background" +msgstr "Nền thẻ" + +msgid "Card shadow" +msgstr "Bóng đổ thẻ" + +msgid "Check for updates" +msgstr "Kiểm tra bản cập nhật" + +msgid "Checking for updates..." +msgstr "Đang kiểm tra bản cập nhật..." + +msgid "Choose color" +msgstr "Chọn màu" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"Chọn hướng văn bản cho các trang xác thực và đăng nhập. Chế độ tự động sẽ " +"chuyển đổi mã ngôn ngữ Ả Rập và Ba Tư (ar, ar_*, ar-*, fa, fa_*, fa-*) sang " +"RTL và sử dụng LTR nếu không tìm thấy hoặc không nhận dạng được." + +msgid "Clean up" +msgstr "Làm sạch" + +msgid "Clean up invalid items" +msgstr "Làm sạch các mục không hợp lệ" + +msgid "Clean up invalid items (%d)" +msgstr "Làm sạch các mục không hợp lệ (%d)" + +msgid "Close" +msgstr "Đóng" + +msgid "Color mode" +msgstr "Chế độ màu" + +msgid "Colors" +msgstr "Màu sắc" + +msgid "Comfortable (42px)" +msgstr "Thoải mái (42px)" + +msgid "Compact (32px)" +msgstr "Ngắn gọn (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "" +"Cấu hình chế độ màu, màu điểm nhấn, kích thước bố cục, hành vi hoạt ảnh, " +"giao diện trang đăng nhập và các tùy chỉnh CSS nâng cao cho " +"luci-theme-fluent." + +msgid "Confirm" +msgstr "Xác nhận" + +msgid "Confirm Installation" +msgstr "Xác nhận cài đặt" + +msgid "Continue" +msgstr "Tiếp tục" + +msgid "Control height" +msgstr "Chiều cao điều khiển" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "" +"Điều khiển độ đậm của phông chữ được sử dụng bởi các nhãn điều hướng chính " +"và các yếu tố văn bản chủ đề liên quan." + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "" +"Điều khiển bán kính góc chung của Fluent được sử dụng bởi thẻ, nút, ô nhập " +"liệu và các thành phần giao diện người dùng liên quan." + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "" +"Điều khiển thời gian chuyển đổi chung của Fluent được sử dụng bởi menu, tiêu " +"đề và các hoạt ảnh chủ đề khác." + +msgid "Copied" +msgstr "Đã sao chép" + +msgid "Copy log" +msgstr "Sao chép nhật ký" + +msgid "Corner radius" +msgstr "Bán kính góc" + +msgid "Current / Pending MAC Address" +msgstr "Địa chỉ MAC hiện tại / Đang chờ xử lý" + +msgid "Custom CSS" +msgstr "CSS tùy chỉnh" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "" +"Tùy chỉnh nền trang đăng nhập, độ mờ của thẻ và bán kính làm mờ cho chế độ " +"sáng và chế độ tối." + +msgid "Dark mode" +msgstr "Chế độ tối" + +msgid "Delete primary menu" +msgstr "Xóa mục chính" + +msgid "Disabled" +msgstr "Đã vô hiệu hóa" + +msgid "Disconnects" +msgstr "Ngắt kết nối" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "" +"Hiển thị bộ tải (loading indicator) theo chủ đề ở cạnh trên trong quá trình " +"tải trang và chuyển đổi." + +msgid "Download and Install" +msgstr "Tải xuống và Cài đặt" + +msgid "Download log" +msgstr "Tải xuống nhật ký" + +msgid "Downloaded" +msgstr "Đã tải xuống" + +msgid "Downloading on router" +msgstr "Đang tải trên router" + +msgid "Drag to move second-level menu" +msgstr "Kéo để sắp xếp mục phụ" + +msgid "Drag to reorder primary menu" +msgstr "Kéo để thay đổi thứ tự mục chính" + +msgid "Drop second-level menus here" +msgstr "Thả các mục phụ vào đây" + +msgid "Edit" +msgstr "Chỉnh sửa" + +msgid "Enable page transition animation" +msgstr "Bật hiệu ứng hoạt ảnh khi chuyển trang" + +msgid "Enable tab underline animation" +msgstr "Bật hiệu ứng hoạt ảnh đường gạch dưới tab" + +msgid "Errors" +msgstr "Lỗi" + +msgid "Expecting: %s" +msgstr "Dự kiến: %s" + +msgid "Extra large (12px)" +msgstr "Rất lớn (12px)" + +msgid "Failed to check for updates" +msgstr "Không thể kiểm tra bản cập nhật." + +msgid "Failed to fetch current theme version." +msgstr "Không thể lấy phiên bản chủ đề hiện tại." + +msgid "Fast" +msgstr "Nhanh" + +msgid "Finished" +msgstr "Hoàn tất" + +msgid "Fix primary menu title errors before saving." +msgstr "Sửa lỗi tiêu đề mục chính trước khi lưu." + +msgid "Fluent Design 2 theme for LuCI" +msgstr "Chủ đề Fluent Design 2 cho LuCI" + +msgid "Fluent Theme" +msgstr "Giao diện Fluent" + +msgid "Fluent theme settings" +msgstr "Cài đặt chủ đề Fluent" + +msgid "Follow system" +msgstr "Theo hệ thống" + +msgid "Force dark mode" +msgstr "Buộc sử dụng chế độ tối" + +msgid "Force light mode" +msgstr "Buộc sử dụng chế độ sáng" + +msgid "Force LTR" +msgstr "Buộc sử dụng hướng từ trái sang phải (LTR)" + +msgid "Force RTL" +msgstr "Buộc sử dụng hướng từ phải sang trái (RTL)" + +msgid "General" +msgstr "Chung" + +msgid "GitHub Token Required" +msgstr "Yêu cầu Token GitHub" + +msgid "Header height" +msgstr "Chiều cao tiêu đề" + +msgid "Height of the top header bar in pixels." +msgstr "Chiều cao của thanh tiêu đề phía trên, tính bằng pixel." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "" +"Màu HEX được sử dụng làm màu nhấn chính của Fluent khi giao diện hiển thị ở " +"chế độ tối." + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "" +"Màu HEX được sử dụng làm màu nhấn chính của Fluent khi giao diện hiển thị ở " +"chế độ sáng." + +msgid "HEX color used for container/card elements in dark mode." +msgstr "Màu HEX được sử dụng cho các thành phần/thẻ trong chế độ tối." + +msgid "HEX color used for container/card elements in light mode." +msgstr "Màu HEX được sử dụng cho các thành phần/thẻ trong chế độ sáng." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "" +"Màu HEX được sử dụng cho nhãn thanh tiến trình khi giao diện hiển thị ở chế " +"độ tối." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "" +"Màu HEX được sử dụng cho nhãn thanh tiến trình khi giao diện hiển thị ở chế " +"độ sáng." + +msgid "HEX color used for the main page background in dark mode." +msgstr "Màu HEX được sử dụng cho nền trang chính trong chế độ tối." + +msgid "HEX color used for the main page background in light mode." +msgstr "Màu HEX được sử dụng cho nền trang chính trong chế độ sáng." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "Màu HEX được sử dụng cho thanh bên điều hướng ở chế độ tối." + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "Màu HEX được sử dụng cho thanh bên điều hướng ở chế độ sáng." + +msgid "Installation cancelled by user." +msgstr "Cài đặt bị hủy bởi người dùng." + +msgid "Installed version" +msgstr "Phiên bản đã cài đặt" + +msgid "Installing package" +msgstr "Đang cài đặt gói" + +msgid "Keep action bar visible while scrolling" +msgstr "Giữ thanh điều khiển hiển thị khi cuộn trang." + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "Giữ các nút Lưu, Áp dụng và Khôi phục cố định ở cuối màn hình khi cuộn." + +msgid "Large" +msgstr "Lớn" + +msgid "Large (8px)" +msgstr "Lớn (8px)" + +msgid "Light mode" +msgstr "Chế độ sáng" + +msgid "Line %d" +msgstr "Dòng %d" + +msgid "Lines" +msgstr "Các dòng" + +msgid "Login" +msgstr "Đăng nhập" + +msgid "Login card opacity" +msgstr "Độ mờ của thẻ đăng nhập" + +msgid "Login page" +msgstr "Trang đăng nhập" + +msgid "Medium" +msgstr "Trung bình" + +msgid "Medium (4px)" +msgstr "Trung bình (4px)" + +msgid "Menu" +msgstr "Menu" + +msgid "Menu title cannot be empty." +msgstr "Tiêu đề menu không được để trống." + +msgid "Navigation font weight" +msgstr "Độ đậm của phông chữ điều hướng" + +msgid "New primary menu" +msgstr "Mục chính mới" + +msgid "New primary menu %d" +msgstr "Mục chính mới %d" + +msgid "Next" +msgstr "Tiếp theo" + +msgid "Nightly build available (reinstallation check)." +msgstr "Có bản dựng Nightly (kiểm tra việc cài đặt lại)." + +msgid "Nightly Channel (Prerelease)" +msgstr "Kênh Nightly (Bản xem trước)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "" +"Không tìm thấy gói phù hợp cho kiến trúc hệ thống của bạn trong bản phát " +"hành này." + +msgid "None" +msgstr "Không" + +msgid "Normal" +msgstr "Bình thường" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Độ mờ của thẻ đăng nhập ở chế độ tối. 0 là hoàn toàn trong suốt và 1 là hoàn " +"toàn không xuyên thấu." + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "" +"Độ mờ của thẻ đăng nhập ở chế độ sáng. 0 là hoàn toàn trong suốt và 1 là " +"hoàn toàn không xuyên thấu." + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "" +"CSS thô tùy chọn được chèn vào mẫu tiêu đề Fluent. Sử dụng điều này để ghi " +"đè các biến --fluent-* bổ sung hoặc tinh chỉnh cụ thể cho trang mà không " +"được hiển thị dưới dạng các tùy chọn chuyên dụng." + +msgid "Package format" +msgstr "Định dạng gói" + +msgid "Page background" +msgstr "Nền trang" + +msgid "Please enter your password." +msgstr "Vui lòng nhập mật khẩu của bạn." + +msgid "Please enter your username." +msgstr "Vui lòng nhập tên người dùng của bạn." + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "" +"Vị trí nhãn của thanh tiến trình so với thanh. Căn chỉnh bắt đầu/kết thúc " +"theo hướng văn bản (trái-phải hoặc phải-trái)." + +msgid "Preset Vendor / Generator" +msgstr "Nhà cung cấp / Trình tạo mặc định" + +msgid "Primary menu title" +msgstr "Tiêu đề menu chính" + +msgid "Primary menu titles cannot be empty." +msgstr "Tiêu đề menu chính không được để trống." + +msgid "Primary menu titles must be unique." +msgstr "Tiêu đề menu chính phải là duy nhất." + +msgid "Progress bar text color" +msgstr "Màu chữ thanh tiến trình" + +msgid "Progress bar text position" +msgstr "Vị trí văn bản trên thanh tiến trình." + +msgid "Random Local MAC" +msgstr "MAC địa phương ngẫu nhiên" + +msgid "Randomize MAC" +msgstr "Ngẫu nhiên hóa MAC" + +msgid "Reload Web Interface" +msgstr "Tải lại giao diện web" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "" +"Thay thế các phần tử select mặc định bằng widget dropdown tùy chỉnh của chủ " +"đề." + +msgid "Respect reduced-motion preference" +msgstr "Tuân thủ cài đặt giảm hiệu ứng chuyển động" + +msgid "Restore %s to its original menu name" +msgstr "Khôi phục %s về tên menu ban đầu" + +msgid "Restore %s to its original menu position" +msgstr "Khôi phục %s về vị trí menu ban đầu" + +msgid "Restore defaults" +msgstr "Khôi phục cài đặt mặc định" + +msgid "Restore to original title" +msgstr "Khôi phục tiêu đề gốc" + +msgid "Retrying with token..." +msgstr "Đang thử lại với token..." + +msgid "Rounded (6px)" +msgstr "Bo tròn (6px)" + +msgid "Save" +msgstr "Lưu" + +msgid "Scroll to bottom" +msgstr "Cuộn đến cuối" + +msgid "Scroll to top" +msgstr "Cuộn lên đầu trang" + +msgid "Search menu items" +msgstr "Tìm kiếm mục menu" + +msgid "Search menu..." +msgstr "Tìm kiếm trong menu..." + +msgid "Second-level menu title" +msgstr "Tiêu đề menu cấp hai" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "Chọn độ sâu bóng áp dụng cho các thẻ và bảng điều khiển theo chủ đề." + +msgid "Select vendor prefix..." +msgstr "Chọn tiền tố nhà cung cấp..." + +msgid "Semibold" +msgstr "Bán đậm" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "" +"Đặt kích thước phông chữ cơ sở của giao diện tính bằng pixel. Hầu hết văn " +"bản của chủ đề được điều chỉnh dựa trên giá trị này thông qua các biến CSS " +"Fluent. Phạm vi khuyến nghị: 12-18px." + +msgid "Show %s in the menu" +msgstr "Hiển thị %s trong menu" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "" +"Hiển thị các thay đổi UCI đang chờ xử lý trong thông báo trạng thái áp dụng " +"ở đầu trang cho đến khi quá trình cấu hình hoàn tất." + +msgid "Show top loading bar" +msgstr "Hiển thị thanh tải ở trên" + +msgid "Show UCI changes while applying configuration" +msgstr "Hiển thị các thay đổi UCI trong khi đang áp dụng cấu hình." + +msgid "Sidebar background" +msgstr "Nền thanh bên" + +msgid "Sidebar width" +msgstr "Chiều rộng thanh bên" + +msgid "Slow" +msgstr "Chậm" + +msgid "Small" +msgstr "Nhỏ" + +msgid "Small (2px)" +msgstr "Nhỏ (2px)" + +msgid "Software Update" +msgstr "Cập nhật phần mềm" + +msgid "Source code" +msgstr "Mã nguồn" + +msgid "Square (0px)" +msgstr "Vuông (0px)" + +msgid "Stable Channel" +msgstr "Kênh ổn định" + +msgid "Starting backend download..." +msgstr "Bắt đầu tải xuống phần mềm nền..." + +msgid "Starting update process..." +msgstr "Bắt đầu quá trình cập nhật..." + +msgid "Submit" +msgstr "Gửi" + +msgid "Switch theme mode" +msgstr "Chuyển đổi chế độ giao diện" + +msgid "Text direction" +msgstr "Hướng văn bản" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "" +"Bố cục menu đã lưu không hợp lệ. Chọn \"Khôi phục cài đặt mặc định\" hoặc " +"chỉnh sửa trước khi lưu." + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "" +"Bố cục menu đã lưu không hợp lệ. Khôi phục cài đặt mặc định hoặc chỉnh sửa " +"bố cục trước khi lưu." + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "" +"Giao diện đang được cài đặt trên router của bạn. Nhật ký hiển thị bên dưới. " +"Tải lại giao diện web sẽ áp dụng các thay đổi sau khi RPCD khởi động lại." + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "Gói giao diện đã sẵn sàng. Bạn có chắc chắn muốn tiếp tục cài đặt không?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "" +"Token này KHÔNG yêu cầu bất kỳ quyền/phạm vi nào (truy cập công khai chỉ đọc " +"là đủ)." + +msgid "Theme settings" +msgstr "Cài đặt giao diện" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "" +"Giao diện đã được cập nhật thành công! Đang tải lại dịch vụ RPC, vui lòng " +"làm mới trang sau 5 giây." + +msgid "Theme transition speed" +msgstr "Tốc độ chuyển tiếp của giao diện" + +msgid "Toggle fullscreen" +msgstr "Chuyển đổi toàn màn hình" + +msgid "Triggering installation on router..." +msgstr "Đang cài đặt trên router..." + +msgid "Update channel" +msgstr "Kênh cập nhật" + +msgid "Update failed" +msgstr "Cập nhật thất bại" + +msgid "Upgrade Successful!" +msgstr "Nâng cấp thành công!" + +msgid "Use Fluent custom select dropdowns" +msgstr "Sử dụng các hộp chọn tùy chỉnh của Fluent" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "" +"Sử dụng View Transition API của trình duyệt để tạo hiệu ứng động khi chuyển " +"giữa các trang LuCI, nếu được hỗ trợ." + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "" +"Sử dụng cài đặt ưu tiên của hệ thống/trình duyệt, hoặc luôn hiển thị giao " +"diện Fluent với bảng màu sáng hoặc tối cố định." + +msgid "Username is required." +msgstr "Tên người dùng là bắt buộc." + +msgid "valid HEX color value" +msgstr "Giá trị màu HEX hợp lệ" + +msgid "Warnings" +msgstr "Cảnh báo" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "" +"Khi bật, các hiệu ứng động của Fluent sẽ tuân theo cài đặt giảm chuyển động " +"của trình duyệt hoặc hệ điều hành." + +msgid "Width of the main navigation sidebar in pixels." +msgstr "Độ rộng của thanh bên điều hướng chính tính bằng pixel." + +msgid "Word wrap" +msgstr "Ngắt dòng chữ" + +msgid "You can create a new token at" +msgstr "Bạn có thể tạo một token mới tại:" + +msgid "Your theme is up to date!" +msgstr "Giao diện của bạn đã ở phiên bản mới nhất." diff --git a/luci-theme-fluent/po/zh-cn b/luci-theme-fluent/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-theme-fluent/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-theme-fluent/po/zh_Hans/fluent.po b/luci-theme-fluent/po/zh_Hans/fluent.po new file mode 100644 index 000000000..717ebcd49 --- /dev/null +++ b/luci-theme-fluent/po/zh_Hans/fluent.po @@ -0,0 +1,699 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:37+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: LazuliKao\n" +"Language-Team: LuCI Theme Fluent\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" + +msgid "%d second-level menus" +msgstr "%d 个二级菜单" + +msgid "A new version is available!" +msgstr "发现有新版本!" + +msgid "About" +msgstr "关于" + +msgid "Above bar, end" +msgstr "上方末尾" + +msgid "Above bar, start" +msgstr "上方起始位置" + +msgid "Accent color" +msgstr "强调色" + +msgid "Add primary menu" +msgstr "添加主菜单" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "调整布局、排版、过渡动画时间、阴影,并在内置控件不足时注入自定义 CSS 变量或规则。" + +msgid "Advanced" +msgstr "高级" + +msgid "Advanced MAC Selector" +msgstr "高级 MAC 选择器" + +msgid "and %d more..." +msgstr "和更多 %d 个..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "在切换原生 LuCI 标签页和主题化标签菜单时,为活动下划线添加动画。" + +msgid "Animation" +msgstr "动画" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "API 调用频率已达上限。请输入 GitHub Token 以继续。此 token 只用于前端请求,不会保存在路由器后端。" + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "适用于整个主题中的标准按钮、输入框、选择框及类似表单控件。" + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "您确定要清除 %d 个无效/已卸载的菜单项或规则吗?" + +msgid "Author" +msgstr "作者" + +msgid "Auto-scroll" +msgstr "自动滚动" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "自动(仅阿拉伯语/波斯语语言环境)" + +msgid "Backdrop blur radius" +msgstr "背景模糊半径" + +msgid "Backend (GHProxy Acceleration)" +msgstr "后端 (GHProxy 加速)" + +msgid "Backend (Official GitHub)" +msgstr "后端 (官方 GitHub)" + +msgid "Base font size" +msgstr "基础字体大小" + +msgid "Below bar, end" +msgstr "下方末尾" + +msgid "Below bar, start" +msgstr "下方起始位置" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "深色模式下,登录卡片背后的模糊半径(单位:像素)。使用 0 可禁用模糊效果。" + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "亮色模式下,登录卡片背后的模糊半径(单位:像素)。使用 0 可禁用模糊效果。" + +msgid "Cancel" +msgstr "取消" + +msgid "Card background" +msgstr "卡片背景" + +msgid "Card shadow" +msgstr "卡片阴影" + +msgid "Check for updates" +msgstr "检查更新" + +msgid "Checking for updates..." +msgstr "正在检查更新..." + +msgid "Choose color" +msgstr "选择颜色" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"为已登录页面和登录页面选择文档方向。自动模式会将阿拉伯语和波斯语语言环境代码(ar、ar_*、ar-*、fa、fa_*、fa-*)解析为 " +"RTL,对于缺失或无法识别的语言环境则回退为 LTR。" + +msgid "Clean up" +msgstr "清除" + +msgid "Clean up invalid items" +msgstr "清除无效项" + +msgid "Clean up invalid items (%d)" +msgstr "清除无效项 (%d)" + +msgid "Close" +msgstr "关闭" + +msgid "Color mode" +msgstr "色彩模式" + +msgid "Colors" +msgstr "颜色" + +msgid "Comfortable (42px)" +msgstr "舒适(42px)" + +msgid "Compact (32px)" +msgstr "紧凑(32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "配置色彩模式、强调色、布局尺寸、动画行为、登录页面外观,以及用于 `luci-theme-fluent` 的高级 CSS 覆盖。" + +msgid "Confirm" +msgstr "确认" + +msgid "Confirm Installation" +msgstr "确认安装" + +msgid "Continue" +msgstr "继续" + +msgid "Control height" +msgstr "控件高度" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "控制主导航标签及相关主题强调文本的字体粗细。" + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "控制卡片、按钮、输入框及相关 UI 元素共享的 Fluent 圆角半径值。" + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "控制菜单、头部和其他主题动画中使用的共享 Fluent 过渡时间。" + +msgid "Copied" +msgstr "已复制" + +msgid "Copy log" +msgstr "复制日志" + +msgid "Corner radius" +msgstr "圆角半径" + +msgid "Current / Pending MAC Address" +msgstr "当前 / 待定 MAC 地址" + +msgid "Custom CSS" +msgstr "自定义 CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "自定义亮色模式和深色模式下登录页面的背景、卡片不透明度和模糊半径。" + +msgid "Dark mode" +msgstr "深色模式" + +msgid "Delete primary menu" +msgstr "删除主菜单" + +msgid "Disabled" +msgstr "禁用" + +msgid "Disconnects" +msgstr "断开" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "在页面加载和过渡期间,在顶部边缘显示主题化的加载指示器。" + +msgid "Download and Install" +msgstr "下载并安装" + +msgid "Download log" +msgstr "下载日志" + +msgid "Downloaded" +msgstr "已下载" + +msgid "Downloading on router" +msgstr "正在路由器上下载..." + +msgid "Drag to move second-level menu" +msgstr "拖动以移动二级菜单" + +msgid "Drag to reorder primary menu" +msgstr "拖动以重新排序主菜单" + +msgid "Drop second-level menus here" +msgstr "将二级菜单拖放到此处" + +msgid "Edit" +msgstr "编辑" + +msgid "Enable page transition animation" +msgstr "启用页面过渡动画" + +msgid "Enable tab underline animation" +msgstr "启用标签页下划线动画" + +msgid "Errors" +msgstr "错误" + +msgid "Expecting: %s" +msgstr "预期:%s" + +msgid "Extra large (12px)" +msgstr "超大 (12像素)" + +msgid "Failed to check for updates" +msgstr "检查更新失败" + +msgid "Failed to fetch current theme version." +msgstr "未能获取当前主题版本。" + +msgid "Fast" +msgstr "快速" + +msgid "Finished" +msgstr "完成" + +msgid "Fix primary menu title errors before saving." +msgstr "保存前,请修复主菜单标题中的错误。" + +msgid "Fluent Design 2 theme for LuCI" +msgstr "适用于 LuCI 的 Fluent Design 2 主题" + +msgid "Fluent Theme" +msgstr "Fluent 主题" + +msgid "Fluent theme settings" +msgstr "Fluent 主题设置" + +msgid "Follow system" +msgstr "跟随系统" + +msgid "Force dark mode" +msgstr "强制深色模式" + +msgid "Force light mode" +msgstr "强制亮色模式" + +msgid "Force LTR" +msgstr "强制 LTR" + +msgid "Force RTL" +msgstr "强制 RTL" + +msgid "General" +msgstr "常规" + +msgid "GitHub Token Required" +msgstr "需要 GitHub Token" + +msgid "Header height" +msgstr "头部高度" + +msgid "Height of the top header bar in pixels." +msgstr "顶部标题栏的高度,单位为像素。" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "在界面以深色模式渲染时,用作 Fluent 主要强调色的十六进制颜色。" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "在界面以亮色模式渲染时,用作 Fluent 主要强调色的十六进制颜色。" + +msgid "HEX color used for container/card elements in dark mode." +msgstr "在深色模式下,用于容器/卡片元素的十六进制颜色。" + +msgid "HEX color used for container/card elements in light mode." +msgstr "在亮色模式下,用于容器/卡片元素的十六进制颜色。" + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "在界面以深色模式渲染时,用于进度条标签的十六进制颜色。" + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "在界面以亮色模式渲染时,用于进度条标签的十六进制颜色。" + +msgid "HEX color used for the main page background in dark mode." +msgstr "在深色模式下,用于主页面的背景的十六进制颜色。" + +msgid "HEX color used for the main page background in light mode." +msgstr "在亮色模式下,用于主页面的背景的十六进制颜色。" + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "深色模式下导航侧边栏使用的十六进制颜色。" + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "亮色模式下导航侧边栏使用的十六进制颜色。" + +msgid "Installation cancelled by user." +msgstr "用户已取消安装。" + +msgid "Installed version" +msgstr "已安装版本" + +msgid "Installing package" +msgstr "正在安装软件包" + +msgid "Keep action bar visible while scrolling" +msgstr "滚动时保持操作栏可见" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "滚动时,将“保存”、“应用”和“重置”操作固定在视口底部。" + +msgid "Large" +msgstr "大号" + +msgid "Large (8px)" +msgstr "大号 (8px)" + +msgid "Light mode" +msgstr "亮色模式" + +msgid "Line %d" +msgstr "第 %d 行" + +msgid "Lines" +msgstr "行" + +msgid "Login" +msgstr "登录" + +msgid "Login card opacity" +msgstr "登录卡片不透明度" + +msgid "Login page" +msgstr "登录页" + +msgid "Medium" +msgstr "中号" + +msgid "Medium (4px)" +msgstr "中号 (4px)" + +msgid "Menu" +msgstr "菜单" + +msgid "Menu title cannot be empty." +msgstr "菜单标题不能为空。" + +msgid "Navigation font weight" +msgstr "导航字体粗细" + +msgid "New primary menu" +msgstr "新建主菜单" + +msgid "New primary menu %d" +msgstr "新建主菜单 %d" + +msgid "Next" +msgstr "下一步" + +msgid "Nightly build available (reinstallation check)." +msgstr "有 Nightly 构建可用(检查是否需要重新安装)。" + +msgid "Nightly Channel (Prerelease)" +msgstr "Nightly 通道 (预发布)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "在此版本中,未找到适用于您系统架构的软件包资源。" + +msgid "None" +msgstr "无" + +msgid "Normal" +msgstr "常规" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "深色模式下登录卡片的不透明度。0 表示完全透明,1 表示完全不透明。" + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "亮色模式下登录卡片的不透明度。0 表示完全透明,1 表示完全不透明。" + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "在 Fluent 头部模板中注入可选的原始 CSS。用于额外的 `--fluent-*` 变量覆盖,或页面特定的调整,这些调整没有作为专门的选项暴露。" + +msgid "Package format" +msgstr "软件包格式" + +msgid "Page background" +msgstr "页面背景" + +msgid "Please enter your password." +msgstr "请输入您的密码。" + +msgid "Please enter your username." +msgstr "请输入您的用户名。" + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "进度条标签相对于进度条的位置。起始/末尾对齐方式会跟随文本方向(从左往右或从右往左)。" + +msgid "Preset Vendor / Generator" +msgstr "预设厂商 / 生成器" + +msgid "Primary menu title" +msgstr "主菜单标题" + +msgid "Primary menu titles cannot be empty." +msgstr "主菜单标题不能为空。" + +msgid "Primary menu titles must be unique." +msgstr "主菜单标题必须唯一。" + +msgid "Progress bar text color" +msgstr "进度条文本颜色" + +msgid "Progress bar text position" +msgstr "进度条文本位置" + +msgid "Random Local MAC" +msgstr "随机本地 MAC" + +msgid "Randomize MAC" +msgstr "随机化 MAC" + +msgid "Reload Web Interface" +msgstr "重新加载网页界面" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "使用主题的自定义下拉组件替换原生的选择元素。" + +msgid "Respect reduced-motion preference" +msgstr "遵循减少动态效果偏好设置" + +msgid "Restore %s to its original menu name" +msgstr "恢复 %s 为其原始菜单名称" + +msgid "Restore %s to its original menu position" +msgstr "恢复 %s 为其原始菜单位置" + +msgid "Restore defaults" +msgstr "恢复默认设置" + +msgid "Restore to original title" +msgstr "恢复为原始标题" + +msgid "Retrying with token..." +msgstr "正在使用令牌重试..." + +msgid "Rounded (6px)" +msgstr "圆角 (6px)" + +msgid "Save" +msgstr "保存" + +msgid "Scroll to bottom" +msgstr "滚动到底部" + +msgid "Scroll to top" +msgstr "滚动到顶部" + +msgid "Search menu items" +msgstr "搜索菜单项" + +msgid "Search menu..." +msgstr "搜索菜单..." + +msgid "Second-level menu title" +msgstr "二级菜单标题" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "选择应用于主题卡片和面板的阴影深度。" + +msgid "Select vendor prefix..." +msgstr "选择厂商前缀..." + +msgid "Semibold" +msgstr "半粗体" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "设置基础界面字体大小(像素)。大多数主题文本会根据 Fluent CSS 变量从该值进行缩放。建议范围:12-18px。" + +msgid "Show %s in the menu" +msgstr "在菜单中显示 %s" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "在应用配置完成前,在顶部应用状态消息中显示待处理的 UCI 变更。" + +msgid "Show top loading bar" +msgstr "显示顶部加载条" + +msgid "Show UCI changes while applying configuration" +msgstr "在应用配置时显示 UCI 变更" + +msgid "Sidebar background" +msgstr "侧边栏背景" + +msgid "Sidebar width" +msgstr "侧边栏宽度" + +msgid "Slow" +msgstr "慢速" + +msgid "Small" +msgstr "小号" + +msgid "Small (2px)" +msgstr "小号 (2px)" + +msgid "Software Update" +msgstr "软件更新" + +msgid "Source code" +msgstr "源代码" + +msgid "Square (0px)" +msgstr "方形 (0px)" + +msgid "Stable Channel" +msgstr "稳定版本通道" + +msgid "Starting backend download..." +msgstr "开始后端下载..." + +msgid "Starting update process..." +msgstr "启动更新过程..." + +msgid "Submit" +msgstr "提交" + +msgid "Switch theme mode" +msgstr "切换主题模式" + +msgid "Text direction" +msgstr "文本方向" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "已存储的菜单布局无效。请选择“恢复默认设置”或编辑后再保存。" + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "已存储的菜单布局无效。恢复默认设置或编辑布局后再保存。" + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "主题正在安装到您的路由器。日志显示在下方。重新加载网页界面将在 RPCD 重启后应用更改。" + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "主题软件包已准备好。您确定要继续安装吗?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "该令牌不需要授予任何权限/范围(只读公开访问权限即可)。" + +msgid "Theme settings" +msgstr "主题设置" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "主题更新成功!正在重新加载 RPC 服务,请在 5 秒后刷新页面。" + +msgid "Theme transition speed" +msgstr "主题过渡速度" + +msgid "Toggle fullscreen" +msgstr "切换全屏" + +msgid "Triggering installation on router..." +msgstr "触发路由器上的安装..." + +msgid "Update channel" +msgstr "更新通道" + +msgid "Update failed" +msgstr "更新失败" + +msgid "Upgrade Successful!" +msgstr "升级成功!" + +msgid "Use Fluent custom select dropdowns" +msgstr "使用 Fluent 自定义下拉选择框" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "当浏览器支持时,使用浏览器的 View Transition API 为 LuCI 页面之间的导航添加过渡动画。" + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "使用系统/浏览器的偏好设置,或始终以固定的亮色或深色配色渲染 Fluent 主题。" + +msgid "Username is required." +msgstr "必须输入用户名。" + +msgid "valid HEX color value" +msgstr "有效的十六进制颜色值" + +msgid "Warnings" +msgstr "警告" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "启用后,Fluent 动画将遵循浏览器或操作系统的减少动态效果偏好设置。" + +msgid "Width of the main navigation sidebar in pixels." +msgstr "主导航侧边栏的宽度(像素)。" + +msgid "Word wrap" +msgstr "自动换行" + +msgid "You can create a new token at" +msgstr "您可以在这里创建新的令牌:" + +msgid "Your theme is up to date!" +msgstr "您的主题已是最新版本!" diff --git a/luci-theme-fluent/po/zh_Hant/fluent.po b/luci-theme-fluent/po/zh_Hant/fluent.po new file mode 100644 index 000000000..da728a431 --- /dev/null +++ b/luci-theme-fluent/po/zh_Hant/fluent.po @@ -0,0 +1,699 @@ +msgid "" +msgstr "" +"Project-Id-Version: luci-theme-fluent\n" +"POT-Creation-Date: 2026-09-05 23:37+0800\n" +"PO-Revision-Date: \n" +"Last-Translator: TranslateGemma\n" +"Language-Team: LuCI Theme Fluent\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" + +msgid "%d second-level menus" +msgstr "%d 個次選單" + +msgid "A new version is available!" +msgstr "已發布新版本!" + +msgid "About" +msgstr "關於" + +msgid "Above bar, end" +msgstr "上方末尾" + +msgid "Above bar, start" +msgstr "上方起始位置" + +msgid "Accent color" +msgstr "強調色" + +msgid "Add primary menu" +msgstr "新增主選單" + +msgid "" +"Adjust layout, typography, transition timing, shadows, and inject custom CSS " +"variables or rules when the built-in controls are not enough." +msgstr "調整版面配置、字體樣式、轉場時機、陰影,以及在內建控制不夠的情況下,注入自訂 CSS 變數或規則。" + +msgid "Advanced" +msgstr "進階" + +msgid "Advanced MAC Selector" +msgstr "進階 MAC 位址選擇器" + +msgid "and %d more..." +msgstr "及更多 %d 項..." + +msgid "" +"Animate the active underline when switching between native LuCI tabs and " +"themed tab menus." +msgstr "在切換原生 LuCI 標籤和主題化標籤菜單時,動態顯示活動底線。" + +msgid "Animation" +msgstr "動畫" + +msgid "" +"API rate limit exceeded. Please enter a GitHub Token to continue. This token " +"is only used for frontend requests and will not be saved on the router " +"backend." +msgstr "API 速率限制已達到。請輸入 GitHub Token 以繼續。此 Token僅用於前端請求,不會儲存在路由器後端。" + +msgid "" +"Applies to standard buttons, inputs, selects, and similar form controls " +"across the theme." +msgstr "適用於標準按鈕、輸入框、選取元件以及主題中類似的表單控制項。" + +msgid "" +"Are you sure you want to clean up %d invalid/uninstalled menu items or " +"rules?" +msgstr "您確定要清除 %d 個無效或未安裝的選單項目或規則嗎?" + +msgid "Author" +msgstr "作者" + +msgid "Auto-scroll" +msgstr "自動滾動" + +msgid "Automatic (Arabic/Persian locales only)" +msgstr "自動 (僅限阿拉伯語/波斯語地區設定)" + +msgid "Backdrop blur radius" +msgstr "背景模糊半徑" + +msgid "Backend (GHProxy Acceleration)" +msgstr "後端 (GHProxy 加速)" + +msgid "Backend (Official GitHub)" +msgstr "後端 (官方 GitHub)" + +msgid "Base font size" +msgstr "基礎字體大小" + +msgid "Below bar, end" +msgstr "下方末尾" + +msgid "Below bar, start" +msgstr "下方起始位置" + +msgid "" +"Blur radius in pixels behind the login card in dark mode. Use 0 to disable " +"blur." +msgstr "深色模式下,登入卡片背後的模糊半徑(以像素為單位)。使用 0 關閉模糊效果。" + +msgid "" +"Blur radius in pixels behind the login card in light mode. Use 0 to disable " +"blur." +msgstr "淺色模式下,登入卡片背後的模糊半徑(以像素為單位)。使用 0 關閉模糊效果。" + +msgid "Cancel" +msgstr "取消" + +msgid "Card background" +msgstr "卡片背景" + +msgid "Card shadow" +msgstr "卡片陰影" + +msgid "Check for updates" +msgstr "檢查更新" + +msgid "Checking for updates..." +msgstr "正在檢查更新..." + +msgid "Choose color" +msgstr "選擇顏色" + +msgid "" +"Choose the document direction for authenticated and login pages. Automatic " +"mode resolves Arabic and Persian locale codes (ar, ar_*, ar-*, fa, fa_*, " +"fa-*) to RTL and falls back to LTR for missing or unrecognized locales." +msgstr "" +"為驗證和登入頁面選擇文件方向。自動模式會將阿拉伯語和波斯語的地區代碼 (ar, ar_*, ar-*, fa, fa_*, fa-*) 解析為從右到左 " +"(RTL),如果缺少或無法識別,則回退為從左到右 (LTR)。" + +msgid "Clean up" +msgstr "清理" + +msgid "Clean up invalid items" +msgstr "清除無效項目" + +msgid "Clean up invalid items (%d)" +msgstr "清除無效項目 (%d)" + +msgid "Close" +msgstr "關閉" + +msgid "Color mode" +msgstr "色彩模式" + +msgid "Colors" +msgstr "顏色" + +msgid "Comfortable (42px)" +msgstr "舒適 (42px)" + +msgid "Compact (32px)" +msgstr "緊湊型 (32px)" + +msgid "" +"Configure color mode, accent colors, layout sizing, animation behavior, " +"login-page appearance, and advanced CSS overrides for luci-theme-fluent." +msgstr "設定色彩模式、強調色、版面配置大小、動畫行為、登入頁面外觀,以及針對 luci-theme-fluent 的進階 CSS 覆寫。" + +msgid "Confirm" +msgstr "確認" + +msgid "Confirm Installation" +msgstr "確認安裝" + +msgid "Continue" +msgstr "繼續" + +msgid "Control height" +msgstr "控制項高度" + +msgid "" +"Controls the font weight used by main navigation labels and related theme " +"text accents." +msgstr "控制用於主要導航標籤以及相關主題文字強調的字體粗細。" + +msgid "" +"Controls the shared Fluent corner radius tokens used by cards, buttons, " +"inputs, and related UI surfaces." +msgstr "控制卡片、按鈕、輸入框和其他 UI 元素的 Fluent 角落圓角值。" + +msgid "" +"Controls the shared Fluent transition timing used by menu, header, and other " +"theme animations." +msgstr "控制菜單、頁首和其他主題動畫使用的 Fluent 過渡時長。" + +msgid "Copied" +msgstr "已複製" + +msgid "Copy log" +msgstr "複製日誌" + +msgid "Corner radius" +msgstr "圓角半徑" + +msgid "Current / Pending MAC Address" +msgstr "目前 / 待處理 MAC 位址" + +msgid "Custom CSS" +msgstr "自訂 CSS" + +msgid "" +"Customize the login page background, card opacity, and blur radius for light " +"and dark modes." +msgstr "自訂登入頁面背景、卡片不透明度,以及淺色和深色模式下的模糊半徑。" + +msgid "Dark mode" +msgstr "深色模式" + +msgid "Delete primary menu" +msgstr "刪除主選單" + +msgid "Disabled" +msgstr "已停用" + +msgid "Disconnects" +msgstr "斷線次數" + +msgid "" +"Display the themed loading indicator at the top edge during page loads and " +"transitions." +msgstr "在頁面載入和轉場時,顯示主題化的載入指示器於頂端。" + +msgid "Download and Install" +msgstr "下載與安裝" + +msgid "Download log" +msgstr "下載日誌" + +msgid "Downloaded" +msgstr "已下載" + +msgid "Downloading on router" +msgstr "正在路由器上下載..." + +msgid "Drag to move second-level menu" +msgstr "拖曳以移動次選單" + +msgid "Drag to reorder primary menu" +msgstr "拖曳以重新排序主選單" + +msgid "Drop second-level menus here" +msgstr "在此處放置次選單" + +msgid "Edit" +msgstr "編輯" + +msgid "Enable page transition animation" +msgstr "啟用頁面轉場動畫" + +msgid "Enable tab underline animation" +msgstr "啟用分頁下劃線動畫" + +msgid "Errors" +msgstr "錯誤" + +msgid "Expecting: %s" +msgstr "預期值: %s" + +msgid "Extra large (12px)" +msgstr "超大 (12px)" + +msgid "Failed to check for updates" +msgstr "檢查更新失敗" + +msgid "Failed to fetch current theme version." +msgstr "無法取得目前主題版本。" + +msgid "Fast" +msgstr "快速" + +msgid "Finished" +msgstr "完成" + +msgid "Fix primary menu title errors before saving." +msgstr "儲存前,請修正主選單標題的錯誤。" + +msgid "Fluent Design 2 theme for LuCI" +msgstr "適用於 LuCI 的 Fluent Design 2 主題" + +msgid "Fluent Theme" +msgstr "Fluent 主題" + +msgid "Fluent theme settings" +msgstr "Fluent 主題設定" + +msgid "Follow system" +msgstr "遵循系統設定" + +msgid "Force dark mode" +msgstr "強制深色模式" + +msgid "Force light mode" +msgstr "強制啟用淺色模式" + +msgid "Force LTR" +msgstr "強制左向右 (LTR)" + +msgid "Force RTL" +msgstr "強制從右向左 (RTL)" + +msgid "General" +msgstr "一般" + +msgid "GitHub Token Required" +msgstr "需要 GitHub 令牌" + +msgid "Header height" +msgstr "頁首高度" + +msgid "Height of the top header bar in pixels." +msgstr "頂部標題欄的高度,單位為像素。" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in dark mode." +msgstr "在深色模式下使用的主要 Fluent 強調色(HEX)。" + +msgid "" +"HEX color used as the primary Fluent accent when the interface is rendered " +"in light mode." +msgstr "在淺色模式下使用的主要 Fluent 強調色(HEX)。" + +msgid "HEX color used for container/card elements in dark mode." +msgstr "在深色模式下用於容器/卡片的顏色 (HEX)." + +msgid "HEX color used for container/card elements in light mode." +msgstr "在淺色模式下用於容器/卡片的顏色 (HEX)." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"dark mode." +msgstr "在深色模式下用於進度條標籤的顏色 (HEX)." + +msgid "" +"HEX color used for progress-bar labels while the interface is rendered in " +"light mode." +msgstr "在淺色模式下用於進度條標籤的顏色 (HEX)." + +msgid "HEX color used for the main page background in dark mode." +msgstr "在深色模式下使用的主要頁面背景顏色 (HEX)." + +msgid "HEX color used for the main page background in light mode." +msgstr "在淺色模式下使用的主要頁面背景顏色 (HEX)." + +msgid "HEX color used for the navigation sidebar in dark mode." +msgstr "深色模式下,導航側邊欄使用的十六進位顏色。" + +msgid "HEX color used for the navigation sidebar in light mode." +msgstr "淺色模式下,導航側邊欄使用的十六進位顏色。" + +msgid "Installation cancelled by user." +msgstr "使用者已取消安裝。" + +msgid "Installed version" +msgstr "已安裝版本" + +msgid "Installing package" +msgstr "正在安裝套件" + +msgid "Keep action bar visible while scrolling" +msgstr "在捲動時保持動作列可見。" + +msgid "" +"Keep Save, Apply, and Reset actions fixed at the bottom of the viewport " +"while scrolling." +msgstr "在捲動時,將「儲存」、「套用」和「重設」動作固定在視窗底端。" + +msgid "Large" +msgstr "大" + +msgid "Large (8px)" +msgstr "大 (8px)" + +msgid "Light mode" +msgstr "淺色模式" + +msgid "Line %d" +msgstr "第 %d 行" + +msgid "Lines" +msgstr "行數" + +msgid "Login" +msgstr "登入" + +msgid "Login card opacity" +msgstr "登入卡片的不透明度" + +msgid "Login page" +msgstr "登入頁面" + +msgid "Medium" +msgstr "中等" + +msgid "Medium (4px)" +msgstr "中等 (4px)" + +msgid "Menu" +msgstr "選單" + +msgid "Menu title cannot be empty." +msgstr "選單標題不能為空。" + +msgid "Navigation font weight" +msgstr "導航字體粗細" + +msgid "New primary menu" +msgstr "新增主選單" + +msgid "New primary menu %d" +msgstr "新增主選單 %d" + +msgid "Next" +msgstr "下一步" + +msgid "Nightly build available (reinstallation check)." +msgstr "有可用的 Nightly 建置版本(檢查是否需要重新安裝)。" + +msgid "Nightly Channel (Prerelease)" +msgstr "Nightly 頻道 (預發布)" + +msgid "" +"No matching package asset found for your system architecture in this " +"release." +msgstr "此版本中未找到與您的系統架構相符的套件資源。" + +msgid "None" +msgstr "無" + +msgid "Normal" +msgstr "正常" + +msgid "" +"Opacity of the login card in dark mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "深色模式下登入卡片的不透明度。0 為完全透明,1 為完全不透明。" + +msgid "" +"Opacity of the login card in light mode. 0 is fully transparent and 1 is " +"fully opaque." +msgstr "淺色模式下登入卡片的不透明度。0 為完全透明,1 為完全不透明。" + +msgid "" +"Optional raw CSS injected into the Fluent header template. Use this for " +"extra --fluent-* variable overrides or page-specific tweaks that are not " +"exposed as dedicated options." +msgstr "可選的原始 CSS,會注入到 Fluent 頁首範本中。 用於額外的 `--fluent-*` 變數覆寫或特定頁面的調整,這些調整沒有提供專用的選項。" + +msgid "Package format" +msgstr "套件格式" + +msgid "Page background" +msgstr "頁面背景" + +msgid "Please enter your password." +msgstr "請輸入您的密碼。" + +msgid "Please enter your username." +msgstr "請輸入您的使用者名稱。" + +msgid "" +"Position of progress-bar labels relative to the bar. Start/end alignment " +"follows the text direction (LTR or RTL)." +msgstr "進度條標籤相對於進度條的位置。起始/末尾對齊方式會隨文字方向(從左往右或從右往左)調整。" + +msgid "Preset Vendor / Generator" +msgstr "預設供應商 / 產生器" + +msgid "Primary menu title" +msgstr "主要選單標題" + +msgid "Primary menu titles cannot be empty." +msgstr "主要選單標題不能為空。" + +msgid "Primary menu titles must be unique." +msgstr "主要選單標題必須唯一。" + +msgid "Progress bar text color" +msgstr "進度條文字顏色" + +msgid "Progress bar text position" +msgstr "進度條文字位置" + +msgid "Random Local MAC" +msgstr "隨機本機 MAC 位址" + +msgid "Randomize MAC" +msgstr "隨機 MAC 位址" + +msgid "Reload Web Interface" +msgstr "重新載入網頁介面" + +msgid "Replace native select elements with the theme's custom dropdown widget." +msgstr "使用主題自訂的下拉式元件,取代原生選取元素。" + +msgid "Respect reduced-motion preference" +msgstr "尊重減少動畫的設定" + +msgid "Restore %s to its original menu name" +msgstr "將 %s 恢復為原始選單名稱" + +msgid "Restore %s to its original menu position" +msgstr "將 %s 恢復為原始選單位置" + +msgid "Restore defaults" +msgstr "還原預設值" + +msgid "Restore to original title" +msgstr "還原為原始標題" + +msgid "Retrying with token..." +msgstr "正在使用 Token 重新嘗試..." + +msgid "Rounded (6px)" +msgstr "圓角 (6px)" + +msgid "Save" +msgstr "儲存" + +msgid "Scroll to bottom" +msgstr "滾動至底部" + +msgid "Scroll to top" +msgstr "滾動至頂部" + +msgid "Search menu items" +msgstr "搜尋選單項目" + +msgid "Search menu..." +msgstr "搜尋選單..." + +msgid "Second-level menu title" +msgstr "第二層選單標題" + +msgid "Select the shadow depth applied to themed cards and panels." +msgstr "選擇套用在主題卡片和面板上的陰影深度。" + +msgid "Select vendor prefix..." +msgstr "選擇供應商前綴..." + +msgid "Semibold" +msgstr "半粗體" + +msgid "" +"Sets the base interface font size in pixels. Most theme text scales from " +"this value through the Fluent CSS variables. Recommended range: 12-18px." +msgstr "設定介面基本字體大小(以像素為單位)。大部分主題文字會透過 Fluent CSS 變數從此值進行縮放。建議範圍:12-18px。" + +msgid "Show %s in the menu" +msgstr "在選單中顯示 %s" + +msgid "" +"Show pending UCI changes in the top apply-status message until the " +"configuration apply completes." +msgstr "在配置應用完成前,於頂端的狀態訊息中顯示所有擱置的 UCI 變更。" + +msgid "Show top loading bar" +msgstr "顯示頂部載入條" + +msgid "Show UCI changes while applying configuration" +msgstr "正在應用配置時,顯示 UCI 變更。" + +msgid "Sidebar background" +msgstr "側邊欄背景" + +msgid "Sidebar width" +msgstr "側邊欄寬度" + +msgid "Slow" +msgstr "慢速" + +msgid "Small" +msgstr "小" + +msgid "Small (2px)" +msgstr "小 (2px)" + +msgid "Software Update" +msgstr "軟體更新" + +msgid "Source code" +msgstr "原始碼" + +msgid "Square (0px)" +msgstr "方形 (0px)" + +msgid "Stable Channel" +msgstr "穩定版本通道" + +msgid "Starting backend download..." +msgstr "正在開始後端下載..." + +msgid "Starting update process..." +msgstr "正在開始更新程序..." + +msgid "Submit" +msgstr "提交" + +msgid "Switch theme mode" +msgstr "切換主題模式" + +msgid "Text direction" +msgstr "文字方向" + +msgid "" +"The stored menu layout is invalid. Choose \"Restore defaults\" or make an edit " +"before saving." +msgstr "儲存的選單配置無效。請選擇「還原預設值」或編輯後再儲存。" + +msgid "" +"The stored menu layout is invalid. Restore defaults or edit the layout " +"before saving." +msgstr "儲存的選單配置無效。還原預設值或編輯配置後再儲存。" + +msgid "" +"The theme is being installed on your router. The logs are displayed below. " +"Reloading the web interface will apply the changes once RPCD restarts." +msgstr "該主題正在安裝到您的路由器。 日誌顯示在下方。 重新載入網頁介面將在 RPCD 重啟後應用更改。" + +msgid "" +"The theme package is ready. Are you sure you want to proceed with the " +"installation?" +msgstr "主題套件已準備好。 您確定要繼續安裝嗎?" + +msgid "" +"The token does NOT require any permissions/scopes to be granted (read-only " +"public access is sufficient)." +msgstr "此權杖不需要任何權限/範圍(僅讀取的公開存取權限即可)。" + +msgid "Theme settings" +msgstr "主題設定" + +msgid "" +"Theme successfully updated! Reloading RPC service, please refresh the page " +"in 5 seconds." +msgstr "主題已成功更新!正在重新載入 RPC 服務,請在 5 秒後刷新頁面。" + +msgid "Theme transition speed" +msgstr "主題轉場速度" + +msgid "Toggle fullscreen" +msgstr "切換全螢幕" + +msgid "Triggering installation on router..." +msgstr "正在路由器上安裝..." + +msgid "Update channel" +msgstr "更新頻道" + +msgid "Update failed" +msgstr "更新失敗" + +msgid "Upgrade Successful!" +msgstr "升級成功!" + +msgid "Use Fluent custom select dropdowns" +msgstr "使用 Fluent 自訂選取式下拉清單" + +msgid "" +"Use the browser View Transition API to animate navigation between LuCI pages " +"when supported." +msgstr "當支援時,使用瀏覽器的 View Transition API 動畫化 LuCI 頁面之間的導航。" + +msgid "" +"Use the system/browser preference, or always render the Fluent theme in a " +"fixed light or dark palette." +msgstr "使用系統/瀏覽器偏好設定,或強制以固定淺色或深色調配色顯示 Fluent 主題。" + +msgid "Username is required." +msgstr "使用者名稱是必填欄位。" + +msgid "valid HEX color value" +msgstr "有效的 HEX 色彩值" + +msgid "Warnings" +msgstr "警告" + +msgid "" +"When enabled, Fluent animations follow the browser or operating system " +"reduced-motion preference." +msgstr "啟用時,Fluent 動畫會遵循瀏覽器或作業系統的減少動畫偏好設定。" + +msgid "Width of the main navigation sidebar in pixels." +msgstr "主要導航側邊欄的寬度(以像素為單位)。" + +msgid "Word wrap" +msgstr "自動換行" + +msgid "You can create a new token at" +msgstr "您可以在這裡建立新的 Token:" + +msgid "Your theme is up to date!" +msgstr "您的主題已是最新版本!" diff --git a/luci-theme-fluent/root/etc/config/fluent b/luci-theme-fluent/root/etc/config/fluent new file mode 100644 index 000000000..c1ce809ec --- /dev/null +++ b/luci-theme-fluent/root/etc/config/fluent @@ -0,0 +1 @@ +config global diff --git a/luci-theme-fluent/root/etc/uci-defaults/luci-fluent b/luci-theme-fluent/root/etc/uci-defaults/luci-fluent new file mode 100755 index 000000000..4b95f66be --- /dev/null +++ b/luci-theme-fluent/root/etc/uci-defaults/luci-fluent @@ -0,0 +1,14 @@ +#!/bin/sh +uci batch </dev/null 2>&1; then + uci set fluent.global=global + uci commit fluent +fi + +mkdir -p /www/luci-static/fluent/background +chmod 0755 /www/luci-static/fluent/background +exit 0 diff --git a/luci-theme-fluent/root/usr/libexec/fluent/online_wallpaper b/luci-theme-fluent/root/usr/libexec/fluent/online_wallpaper new file mode 100755 index 000000000..70bb0897b --- /dev/null +++ b/luci-theme-fluent/root/usr/libexec/fluent/online_wallpaper @@ -0,0 +1,58 @@ +#!/bin/sh +# author jjm2473, customized for fluent + +WEB_PIC_SRC=$(uci -q get fluent.@global[0].login_bg || echo 'bing') + +if [ "$WEB_PIC_SRC" != "bing" ]; then + exit 0 +fi + +CACHE=/var/run/fluent_${WEB_PIC_SRC}.url +WRLOCK=/var/lock/fluent_${WEB_PIC_SRC}.lock + +fetch_pic_url() { + local picpath=$(curl -fks --max-time 3 \ + "https://www.bing.com/HPImageArchive.aspx?format=js&n=1" | + jsonfilter -qe '@.images[0].url') + [ -n "${picpath}" ] && echo "https://www.bing.com${picpath}" +} + +try_update() { + local lock="$WRLOCK" + exec 200>$lock + + if flock -n 200 >/dev/null 2>&1; then + local picurl=$(fetch_pic_url) + if [ -n "$picurl" ]; then + echo "${picurl}" | tee "$CACHE" + else + if [ -s "$CACHE" ]; then + cat "$CACHE" + else + touch "$CACHE" + fi + fi + flock -u 200 >/dev/null 2>&1 + elif [ -s "$CACHE" ]; then + cat "$CACHE" + fi +} + +get_url() { + if [ -f "$CACHE" ]; then + local idle_t=$(($(date '+%s') - $(date -r "$CACHE" '+%s' 2>/dev/null || echo '0'))) + if [ -s "$CACHE" ]; then + if [ $idle_t -le 43200 ]; then + cat "$CACHE" + return + fi + else + if [ $idle_t -le 120 ]; then + return + fi + fi + fi + try_update +} + +get_url diff --git a/luci-theme-fluent/root/usr/libexec/rpcd/luci.fluent b/luci-theme-fluent/root/usr/libexec/rpcd/luci.fluent new file mode 100755 index 000000000..e5fcf9ff2 --- /dev/null +++ b/luci-theme-fluent/root/usr/libexec/rpcd/luci.fluent @@ -0,0 +1,274 @@ +#!/bin/sh + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +if command -v apk >/dev/null 2>&1; then + PKG_EXT=".apk" +else + PKG_EXT=".ipk" +fi + +readonly bg_path="/www/luci-static/fluent/background" +readonly tmp_path="/tmp/fluent_background.tmp" +readonly update_pkg="/tmp/fluent-update${PKG_EXT}" +readonly i18n_pkg="/tmp/fluent-i18n-update${PKG_EXT}" + +case "$1" in + "list") + json_init + json_add_object "avail" + json_close_object + json_add_object "remove" + json_add_string "filename" "filename" + json_close_object + json_add_object "rename" + json_add_string "newname" "filename" + json_close_object + json_add_object "set_mode" + json_add_string "mode" "mode" + json_close_object + json_add_object "get_version" + json_close_object + json_add_object "start_download" + json_add_string "url" "url" + json_add_string "i18n_url" "i18n_url" + json_add_string "i18n_urls" "i18n_urls" + json_close_object + json_add_object "check_download" + json_close_object + json_add_object "do_install" + json_add_string "hash" "hash" + json_add_string "i18n_hash" "i18n_hash" + json_add_string "i18n_hashes" "i18n_hashes" + json_close_object + json_add_object "get_install_log" + json_close_object + json_dump + json_cleanup + ;; +"call") + case "$2" in + "avail") + json_init + json_add_int "avail" "$(df | grep -E '/$' | awk '{print $4}')" + json_dump + json_cleanup + ;; + "remove") + read -r input + json_load "$input" + json_get_var filename "filename" + json_cleanup + if dirname "$filename" | grep -q ".."; then + echo '{ "result": 255 }' + exit 255 + fi + rm -f "$bg_path/$filename" + echo '{ "result": 0 }' + ;; + "rename") + read -r input + json_load "$input" + json_get_var newname "newname" + json_cleanup + if dirname "$newname" | grep -q ".."; then + echo '{ "result": 255 }' + exit 255 + fi + mkdir -p "$bg_path" + if mv "$tmp_path" "$bg_path/$newname" 2>"/dev/null"; then + chmod 0644 "$bg_path/$newname" + echo '{ "result": 0 }' + else + echo '{ "result": 1 }' + fi + ;; + "set_mode") + read -r input + json_load "$input" + json_get_var mode "mode" + json_cleanup + case "$mode" in + light|dark|auto) ;; + *) + echo '{ "result": 255 }' + exit 255 + ;; + esac + if uci -q set fluent.@global[0].mode="$mode" && uci -q commit fluent; then + echo '{ "result": 0 }' + else + echo '{ "result": 1 }' + fi + ;; + "get_version") + version="" + pkg_type="ipk" + i18n_installed=0 + installed_i18n="" + if command -v apk >/dev/null 2>&1; then + pkg_type="apk" + version=$(apk list -I luci-theme-fluent 2>/dev/null | awk '{print $1}' | sed 's/^luci-theme-fluent-//' | head -1) + installed_i18n=$(apk list -I 2>/dev/null | grep -E '^luci-i18n-fluent-' | awk '{print $1}' | sed 's/^luci-i18n-fluent-//') + else + version=$(opkg status luci-theme-fluent 2>/dev/null | grep -m1 'Version:' | awk '{print $2}') + installed_i18n=$(opkg list-installed 2>/dev/null | grep -E '^luci-i18n-fluent-' | awk '{print $1}' | sed 's/^luci-i18n-fluent-//') + fi + if echo "$installed_i18n" | grep -q "^zh-cn$"; then + i18n_installed=1 + fi + if [ -z "$version" ]; then + version="1.0.1-dev" + fi + json_init + json_add_string "version" "$version" + json_add_string "pkg_type" "$pkg_type" + json_add_boolean "i18n_zh_cn_installed" "$i18n_installed" + json_add_array "installed_i18n" + for lang in $installed_i18n; do + json_add_string "" "$lang" + done + json_close_array + json_dump + json_cleanup + ;; + "start_download") + read -r input + json_load "$input" + json_get_var url "url" + json_get_var i18n_url "i18n_url" + json_get_var i18n_urls "i18n_urls" + json_cleanup + + rm -f "$update_pkg" + rm -f /tmp/fluent-i18n-* + rm -f /tmp/fluent-update.log + + if [ -n "$url" ]; then + download_cmd="" + if command -v uclient-fetch >/dev/null 2>&1; then + download_cmd="uclient-fetch -O $update_pkg \"$url\"" + urls_to_dl="${i18n_urls:-$i18n_url}" + idx=1 + for u in $urls_to_dl; do + download_cmd="$download_cmd && uclient-fetch -O /tmp/fluent-i18n-${idx}${PKG_EXT} \"$u\"" + idx=$((idx + 1)) + done + else + download_cmd="wget -O $update_pkg \"$url\"" + urls_to_dl="${i18n_urls:-$i18n_url}" + idx=1 + for u in $urls_to_dl; do + download_cmd="$download_cmd && wget -O /tmp/fluent-i18n-${idx}${PKG_EXT} \"$u\"" + idx=$((idx + 1)) + done + fi + nohup sh -c "$download_cmd >/tmp/fluent-update.log 2>&1" >/dev/null 2>&1 & + echo '{ "result": 0 }' + else + echo '{ "result": 255, "message": "Missing URL" }' + fi + ;; + "check_download") + json_init + size1=$(ls -l $update_pkg 2>/dev/null | awk '{print $5}') + size2=$(ls -l /tmp/fluent-i18n-* 2>/dev/null | awk '{sum+=$5} END {print sum}') + size=$(( ${size1:-0} + ${size2:-0} )) + json_add_int "size" "$size" + + if pgrep -f "fluent-update" >/dev/null; then + json_add_boolean "running" 1 + else + json_add_boolean "running" 0 + if [ "$size" -eq 0 ]; then + # If process exited and size is 0, download failed + json_add_int "code" 255 + else + json_add_int "code" 0 + fi + fi + json_dump + json_cleanup + ;; + "do_install") + read -r input + json_load "$input" + json_get_var hash "hash" + json_get_var i18n_hash "i18n_hash" + json_get_var i18n_hashes "i18n_hashes" + json_cleanup + + if [ -z "$hash" ]; then + echo '{ "result": 255, "message": "Missing hash parameter" }' + exit 255 + fi + + if [ ! -f "$update_pkg" ]; then + echo '{ "result": 255, "message": "Package file not found" }' + exit 255 + fi + + if [ "$hash" != "skip" ]; then + if ! echo "$hash" | grep -qE '^[0-9a-fA-F]{64}$'; then + echo '{ "result": 255, "message": "Invalid hash parameter" }' + exit 255 + fi + actual_hash=$(sha256sum "$update_pkg" | awk '{print $1}') + if [ "$actual_hash" != "$hash" ]; then + echo '{ "result": 255, "message": "Hash verification failed" }' + exit 255 + fi + fi + + hashes_to_check="${i18n_hashes:-$i18n_hash}" + if [ -n "$hashes_to_check" ]; then + idx=1 + for h in $hashes_to_check; do + target_i18n_pkg="/tmp/fluent-i18n-${idx}${PKG_EXT}" + if [ -f "$target_i18n_pkg" ] && [ "$h" != "skip" ]; then + if ! echo "$h" | grep -qE '^[0-9a-fA-F]{64}$'; then + echo '{ "result": 255, "message": "Invalid i18n hash parameter" }' + exit 255 + fi + actual_i18n_hash=$(sha256sum "$target_i18n_pkg" | awk '{print $1}') + if [ "$actual_i18n_hash" != "$h" ]; then + echo '{ "result": 255, "message": "i18n Hash verification failed" }' + exit 255 + fi + fi + idx=$((idx + 1)) + done + fi + + install_cmd="" + pkgs="$update_pkg" + i18n_files=$(ls /tmp/fluent-i18n-* 2>/dev/null) + if [ -n "$i18n_files" ]; then + pkgs="$pkgs $i18n_files" + fi + + if command -v apk >/dev/null 2>&1; then + install_cmd="apk add --allow-untrusted $pkgs" + else + install_cmd="opkg install $pkgs" + fi + + nohup sh -c "sleep 1 && $install_cmd > /tmp/fluent-install.log 2>&1 ; rm -f $pkgs ; /etc/init.d/rpcd restart" >/dev/null 2>&1 & + + echo '{ "result": 0 }' + ;; + "get_install_log") + json_init + if [ -f "/tmp/fluent-install.log" ]; then + json_add_string "log" "$(cat /tmp/fluent-install.log)" + else + json_add_string "log" "" + fi + json_dump + json_cleanup + ;; + esac + ;; +esac + diff --git a/luci-theme-fluent/root/usr/share/luci/menu.d/luci-theme-fluent.json b/luci-theme-fluent/root/usr/share/luci/menu.d/luci-theme-fluent.json new file mode 100644 index 000000000..819aa7878 --- /dev/null +++ b/luci-theme-fluent/root/usr/share/luci/menu.d/luci-theme-fluent.json @@ -0,0 +1,14 @@ +{ + "admin/system/fluent-config": { + "title": "Fluent Theme", + "order": 90, + "action": { + "type": "view", + "path": "fluent-config" + }, + "depends": { + "acl": [ "luci-theme-fluent" ], + "uci": { "fluent": true } + } + } +} diff --git a/luci-theme-fluent/root/usr/share/rpcd/acl.d/luci-theme-fluent.json b/luci-theme-fluent/root/usr/share/rpcd/acl.d/luci-theme-fluent.json new file mode 100644 index 000000000..c991b0f8b --- /dev/null +++ b/luci-theme-fluent/root/usr/share/rpcd/acl.d/luci-theme-fluent.json @@ -0,0 +1,25 @@ +{ + "luci-theme-fluent": { + "description": "Grant UCI and file access for luci-theme-fluent config view", + "read": { + "file": { + "/www/luci-static/fluent/background/*": [ "list" ] + }, + "ubus": { + "luci.fluent": [ "avail", "remove", "rename", "set_mode", "get_version", "check_download", "get_install_log" ] + }, + "uci": [ "fluent" ] + }, + "write": { + "file": { + "/tmp/fluent_background.tmp": [ "write" ], + "/tmp/fluent-update.ipk": [ "write" ], + "/tmp/fluent-update.apk": [ "write" ] + }, + "ubus": { + "luci.fluent": [ "set_mode", "do_install", "start_download" ] + }, + "uci": [ "fluent" ] + } + } +} diff --git a/luci-theme-fluent/ucode/template/themes/fluent/defaults.ut b/luci-theme-fluent/ucode/template/themes/fluent/defaults.ut new file mode 100644 index 000000000..bbeddcddd --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/defaults.ut @@ -0,0 +1,36 @@ +{% + defaults.mode = "auto"; + defaults.primary = "#0078d4"; + defaults.dark_primary = "#4da6ff"; + defaults.progressbar_font = "#2e2b60"; + defaults.dark_progressbar_font = "#d6d9e5"; + defaults.page_bg = "#fafafa"; + defaults.card_bg = "#ffffff"; + defaults.sidebar_bg = "#f3f3f3"; + defaults.dark_page_bg = "#1b1b1b"; + defaults.dark_card_bg = "#2d2d2d"; + defaults.dark_sidebar_bg = "#1f1f1f"; + defaults.font_weight = "normal"; + defaults.font_size = "14"; + defaults.border_radius = "4"; + defaults.control_height = "32"; + defaults.sidebar_width = "260"; + defaults.header_height = "48"; + defaults.login_bg = "microsoft"; + defaults.blur = "40"; + defaults.transparency = "0.8"; + defaults.blur_dark = "20"; + defaults.transparency_dark = "0.8"; + defaults.card_shadow = "small"; + defaults.transition_speed = "normal"; + defaults.custom_css = ""; + defaults.custom_select = "1"; + defaults.view_transition = "1"; + defaults.tab_animation = "1"; + defaults.prefers_reduced_motion = "1"; + defaults.loading_bar = "1"; + defaults.sticky_action_bar = "1"; + defaults.uci_changes_preview = "1"; + defaults.direction_mode = "auto"; + defaults.progressbar_text_position = "top-start"; +%} diff --git a/luci-theme-fluent/ucode/template/themes/fluent/direction.ut b/luci-theme-fluent/ucode/template/themes/fluent/direction.ut new file mode 100644 index 000000000..13b4c5736 --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/direction.ut @@ -0,0 +1,25 @@ +{% + let configured_direction_mode = cfg.get_first('fluent', 'global', 'direction_mode') || defaults.direction_mode; + let direction_mode = configured_direction_mode; + + if (direction_mode !== 'rtl' && direction_mode !== 'ltr' && direction_mode !== 'auto') { + direction_mode = defaults.direction_mode; + } + + let locale = dispatcher.lang || ''; + let resolved_direction = 'ltr'; + + if (direction_mode === 'rtl') { + resolved_direction = 'rtl'; + } else if (direction_mode === 'auto') { + if (locale === 'ar' || locale === 'fa' || + index(locale, 'ar_') === 0 || index(locale, 'ar-') === 0 || + index(locale, 'fa_') === 0 || index(locale, 'fa-') === 0) { + resolved_direction = 'rtl'; + } + } + + direction.mode = direction_mode; + direction.locale = locale; + direction.resolved = resolved_direction; +%} diff --git a/luci-theme-fluent/ucode/template/themes/fluent/footer.ut b/luci-theme-fluent/ucode/template/themes/fluent/footer.ut new file mode 100644 index 000000000..737dfd79c --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/footer.ut @@ -0,0 +1,39 @@ +{# + Fluent Theme for LuCI - Footer + Licensed to the public under the Apache License 2.0 +#} + + +{% if (!ctx._fluent_login_shown) { %} +
+{% if (!ctx.authsession) { %} + +{% } %} +
+ + + + + +{% } %} diff --git a/luci-theme-fluent/ucode/template/themes/fluent/footer_login.ut b/luci-theme-fluent/ucode/template/themes/fluent/footer_login.ut new file mode 100644 index 000000000..690d7d074 --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/footer_login.ut @@ -0,0 +1,17 @@ +{# + Fluent Theme for LuCI - Login Footer + Licensed to the public under the Apache License 2.0 +#} + + + + + + + diff --git a/luci-theme-fluent/ucode/template/themes/fluent/header.ut b/luci-theme-fluent/ucode/template/themes/fluent/header.ut new file mode 100644 index 000000000..08d8d5ab4 --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/header.ut @@ -0,0 +1,496 @@ +{# + Fluent Theme for LuCI + Based on luci-theme-argon patterns, translated to ucode syntax + + Licensed to the public under the Apache License 2.0 +#} + +{% + import { access } from 'fs'; + import { cursor } from 'uci'; + import { getuid, getspnam } from 'luci.core'; + import { srand } from 'math'; + + //send as HTML5 + http.prepare_content('text/html; charset=UTF-8'); + + srand(+substr(reverse(time() + ""), 0, 8)); + + // System info + let boardinfo = ubus.call('system', 'board'); + let hostname = boardinfo && boardinfo.hostname ? boardinfo.hostname : '?'; + let model = boardinfo && boardinfo.model ? boardinfo.model : (boardinfo && boardinfo.board_name ? boardinfo.board_name : '?'); + + let cfg = cursor(); + let defaults = {}; + include('themes/fluent/defaults', { defaults }); + let mode = defaults.mode; + let primary = defaults.primary; + let dark_primary = defaults.dark_primary; + let blur_radius = defaults.blur; + let blur_radius_dark = defaults.blur_dark; + let blur_opacity = defaults.transparency; + let blur_opacity_dark = defaults.transparency_dark; + let font_weight = defaults.font_weight; + let font_size = defaults.font_size; + let progressbar_font = defaults.progressbar_font; + let dark_progressbar_font = defaults.dark_progressbar_font; + let main_left_font_color = '#69758c'; + let bar_color = primary; + let sidebar_width = defaults.sidebar_width; + let header_height = defaults.header_height; + let border_radius = defaults.border_radius; + let card_shadow = defaults.card_shadow; + let control_height = defaults.control_height; + let transition_speed = defaults.transition_speed; + let custom_css = defaults.custom_css; + let custom_select = defaults.custom_select; + let view_transition = defaults.view_transition; + let tab_animation = defaults.tab_animation; + let prefers_reduced_motion = defaults.prefers_reduced_motion; + let loading_bar = defaults.loading_bar; + let sticky_action_bar = defaults.sticky_action_bar; + let page_bg = defaults.page_bg; + let card_bg = defaults.card_bg; + let sidebar_bg = defaults.sidebar_bg; + let dark_page_bg = defaults.dark_page_bg; + let dark_card_bg = defaults.dark_card_bg; + let dark_sidebar_bg = defaults.dark_sidebar_bg; + let progressbar_text_position = defaults.progressbar_text_position; + let menu_layout = defaults.menu_layout || null; + let direction = {}; + include('themes/fluent/direction', { cfg, defaults, direction }); + + // Transition speed mapping + let transition_map = { + 'fast': '100ms', + 'normal': '150ms', + 'slow': '250ms', + 'none': '0ms' + }; + + // Shadow mapping (utilizing CSS variables for dynamic mode-aware soft shadows) + let shadow_map = { + 'none': 'none', + 'small': 'var(--fluent-shadow-sm)', + 'medium': 'var(--fluent-shadow-md)', + 'large': 'var(--fluent-shadow-lg)' + }; + + if (access('/etc/config/fluent')) { + mode = cfg.get_first('fluent', 'global', 'mode') || mode; + primary = cfg.get_first('fluent', 'global', 'primary') || primary; + dark_primary = cfg.get_first('fluent', 'global', 'dark_primary') || dark_primary; + font_weight = cfg.get_first('fluent', 'global', 'font_weight') || font_weight; + font_size = cfg.get_first('fluent', 'global', 'font_size') || font_size; + blur_radius = cfg.get_first('fluent', 'global', 'blur') || blur_radius; + blur_radius_dark = cfg.get_first('fluent', 'global', 'blur_dark') || blur_radius_dark; + blur_opacity = cfg.get_first('fluent', 'global', 'transparency') || blur_opacity; + blur_opacity_dark = cfg.get_first('fluent', 'global', 'transparency_dark') || blur_opacity_dark; + progressbar_font = cfg.get_first('fluent', 'global', 'progressbar_font') || progressbar_font; + dark_progressbar_font = cfg.get_first('fluent', 'global', 'dark_progressbar_font') || dark_progressbar_font; + sidebar_width = cfg.get_first('fluent', 'global', 'sidebar_width') || sidebar_width; + header_height = cfg.get_first('fluent', 'global', 'header_height') || header_height; + border_radius = cfg.get_first('fluent', 'global', 'border_radius') || border_radius; + card_shadow = cfg.get_first('fluent', 'global', 'card_shadow') || card_shadow; + control_height = cfg.get_first('fluent', 'global', 'control_height') || control_height; + transition_speed = cfg.get_first('fluent', 'global', 'transition_speed') || transition_speed; + custom_css = cfg.get_first('fluent', 'global', 'custom_css') || custom_css; + page_bg = cfg.get_first('fluent', 'global', 'page_bg') || page_bg; + card_bg = cfg.get_first('fluent', 'global', 'card_bg') || card_bg; + sidebar_bg = cfg.get_first('fluent', 'global', 'sidebar_bg') || sidebar_bg; + dark_page_bg = cfg.get_first('fluent', 'global', 'dark_page_bg') || dark_page_bg; + dark_card_bg = cfg.get_first('fluent', 'global', 'dark_card_bg') || dark_card_bg; + dark_sidebar_bg = cfg.get_first('fluent', 'global', 'dark_sidebar_bg') || dark_sidebar_bg; + custom_select = cfg.get_first('fluent', 'global', 'custom_select') || custom_select; + view_transition = cfg.get_first('fluent', 'global', 'view_transition') || view_transition; + tab_animation = cfg.get_first('fluent', 'global', 'tab_animation') || tab_animation; + prefers_reduced_motion = cfg.get_first('fluent', 'global', 'prefers_reduced_motion') || prefers_reduced_motion; + loading_bar = cfg.get_first('fluent', 'global', 'loading_bar') || loading_bar; + sticky_action_bar = cfg.get_first('fluent', 'global', 'sticky_action_bar') || sticky_action_bar; + progressbar_text_position = cfg.get_first('fluent', 'global', 'progressbar_text_position') || progressbar_text_position; + menu_layout = cfg.get_first('fluent', 'global', 'menu_layout') || null; + + bar_color = mode === 'dark' ? dark_primary : primary; + main_left_font_color = font_weight === 'normal' ? '#5f6368' : '#69758c'; + } + + // Computed values + let transition_duration = transition_map[transition_speed] || '150ms'; + let card_shadow_value = shadow_map[card_shadow] || shadow_map['small']; + let brand_name = hostname; + let page_title = striptags(hostname + (node && node.title ? ' - ' + _(node.title) : '')); +%} {% if (ctx.authsession) { %} + + + + + + {{ page_title }} - LuCI + + + + + + + + + + + + + + + + + + + + + {% if (mode === 'auto') { %} + + + {% } else if (mode === 'dark') { %} + + {% } else if (mode === 'light') { %} + + {% } %} + + {% if (access('/www/luci-static/fluent/css/fluent.css.gz')) { %} + + + {% } else { %} + + {% } %} + + + {% if (view_transition == '1') { %} + + {% } %} + + {% if (loading_bar == '1') { %} + + {% } %} + + + {% if (node && node.css) { %} + + {% } %} + {% if (css) { %} + + {% } %} + + + + + + + + {% if (ctx.authsession) { %} + + {% } %} + + {% if (loading_bar == '1') { %} + + {% } %} + +
+ {% if (ctx.authsession) { %} + + + {% } %} +
+
+ + {% if (ctx.authsession) { %} + +
+ +
+
+ {% } %} +
+
+
+
+ {% if (getuid() == 0 && getspnam('root') && getspnam('root').pwdp === '' && join('-', ctx.request_path) != 'admin-system-admin') { %} +
+

{{ _('No password set!') }}

+

{{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}

+ {% if (dispatcher.lookup('admin/system/admin')) { %} + + {% } %} +
+ {% } %} + + + + + +{% } else { %} +{% include("themes/fluent/out_header_login"); %} +{% include("themes/fluent/sysauth"); %} +{% ctx._fluent_login_shown = true; %} +{% } %} diff --git a/luci-theme-fluent/ucode/template/themes/fluent/header_login.ut b/luci-theme-fluent/ucode/template/themes/fluent/header_login.ut new file mode 100644 index 000000000..805ddc8b6 --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/header_login.ut @@ -0,0 +1,145 @@ +{# + Fluent Theme for LuCI - Login Page Header + Licensed to the public under the Apache License 2.0 +#} + +{% + import { access } from 'fs'; + import { cursor } from 'uci'; + import { srand } from 'math'; + + // System info + let boardinfo = ubus.call('system', 'board'); + let hostname = boardinfo && boardinfo.hostname ? boardinfo.hostname : '?'; + + let cfg = cursor(); + let defaults = {}; + include('themes/fluent/defaults', { defaults }); + + http.prepare_content('text/html; charset=UTF-8'); + + srand(+substr(reverse(time() + ""), 0, 8)); + + let mode = defaults.mode; + let primary = defaults.primary; + let dark_primary = defaults.dark_primary; + let blur_radius = defaults.blur; + let blur_radius_dark = defaults.blur_dark; + let blur_opacity = defaults.transparency; + let blur_opacity_dark = defaults.transparency_dark; + let bar_color = primary; + let direction = {}; + include('themes/fluent/direction', { cfg, defaults, direction }); + + if (access('/etc/config/fluent')) { + primary = cfg.get_first('fluent', 'global', 'primary') || primary; + dark_primary = cfg.get_first('fluent', 'global', 'dark_primary') || dark_primary; + mode = cfg.get_first('fluent', 'global', 'mode') || mode; + blur_radius = cfg.get_first('fluent', 'global', 'blur') || blur_radius; + blur_radius_dark = cfg.get_first('fluent', 'global', 'blur_dark') || blur_radius_dark; + blur_opacity = cfg.get_first('fluent', 'global', 'transparency') || blur_opacity; + blur_opacity_dark = cfg.get_first('fluent', 'global', 'transparency_dark') || blur_opacity_dark; + bar_color = (mode === 'dark') ? dark_primary : primary; + } +%} + + + + + + {{ hostname }} - LuCI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if (mode === 'auto') { %} + + + {% } else if (mode === 'dark') { %} + + + {% } else if (mode === 'light') { %} + + + {% } %} + + {% if (access('/www/luci-static/fluent/css/fluent.css.gz')) { %} + + + {% } else { %} + + {% } %} + + + + + + + + diff --git a/luci-theme-fluent/ucode/template/themes/fluent/out_header_login.ut b/luci-theme-fluent/ucode/template/themes/fluent/out_header_login.ut new file mode 100644 index 000000000..8c5766715 --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/out_header_login.ut @@ -0,0 +1,11 @@ +{# + Fluent Theme for LuCI - Login Header Wrapper + Licensed to the public under the Apache License 2.0 +#} + +{% + if (!ctx.template_header_sent) { + include("themes/fluent/header_login"); + ctx.template_header_sent = true; + } +%} diff --git a/luci-theme-fluent/ucode/template/themes/fluent/sysauth.ut b/luci-theme-fluent/ucode/template/themes/fluent/sysauth.ut new file mode 100644 index 000000000..ebfc5ed81 --- /dev/null +++ b/luci-theme-fluent/ucode/template/themes/fluent/sysauth.ut @@ -0,0 +1,565 @@ +{% include('themes/fluent/out_header_login'); %} +{# + Fluent Theme for LuCI - Login Page + Licensed to the public under the Apache License 2.0 +#} + +{% + import { access, glob, popen } from 'fs'; + import { cursor } from 'uci'; + + // System info + let boardinfo = ubus.call('system', 'board'); + let hostname = boardinfo && boardinfo.hostname ? boardinfo.hostname : '?'; + + let cfg = cursor(); + let defaults = {}; + include('themes/fluent/defaults', { defaults }); + let mode = defaults.mode; + let primary = defaults.primary; + let dark_primary = defaults.dark_primary; + let blur_radius = defaults.blur; + let blur_radius_dark = defaults.blur_dark; + let blur_opacity = defaults.transparency; + let blur_opacity_dark = defaults.transparency_dark; + let bar_color = primary; + + if (access('/etc/config/fluent')) { + mode = cfg.get_first('fluent', 'global', 'mode') || mode; + primary = cfg.get_first('fluent', 'global', 'primary') || primary; + dark_primary = cfg.get_first('fluent', 'global', 'dark_primary') || dark_primary; + blur_radius = cfg.get_first('fluent', 'global', 'blur') || blur_radius; + blur_radius_dark = cfg.get_first('fluent', 'global', 'blur_dark') || blur_radius_dark; + blur_opacity = cfg.get_first('fluent', 'global', 'transparency') || blur_opacity; + blur_opacity_dark = cfg.get_first('fluent', 'global', 'transparency_dark') || blur_opacity_dark; + + bar_color = mode === 'dark' ? dark_primary : primary; + } + + // Background image handling + let imageTypes = { + jpg: true, png: true, gif: true, webp: true + }; + let videoTypes = { + mp4: true, webm: true + }; + + function fetchMedia(path, themeDir) { + let backgroundTable = []; + let backgroundCount = 0; + + try { + let files = glob(path); + for (let f in files) { + let m = match(f, regexp('.+\\.([A-Za-z0-9]+)$')); + let ext = m ? m[1] : null; + + if (ext in imageTypes || ext in videoTypes) { + let parts = split(f, '/'); + let filename = parts[length(parts) - 1]; + push(backgroundTable, { + type: ext, + is_video: ext in videoTypes, + url: themeDir + filename + }); + backgroundCount++; + } + } + } catch (e) {} + + return [backgroundTable, backgroundCount]; + } + + function selectBackground(themeDir) { + let bgUrl = ''; + let backgroundType = 'None'; + let mimeType = ''; + let login_bg = defaults.login_bg; + + if (access('/etc/config/fluent')) { + login_bg = cfg.get_first('fluent', 'global', 'login_bg') || login_bg; + } + + if (login_bg === 'microsoft') { + return ['', 'MicrosoftDynamic', '']; + } + + if (login_bg === 'bing') { + try { + let proc = popen('/usr/libexec/fluent/online_wallpaper'); + let picurl = proc ? trim(proc.read('all')) : ''; + if (proc) + proc.close(); + if (picurl) { + return [picurl, 'Image', '']; + } + } catch (e) {} + } + + if (login_bg !== 'custom') { + return ['', 'None', '']; + } + + // Custom mode: rotate uploaded media from the background directory + let mediaFiles = fetchMedia('/www' + themeDir + '*', themeDir); + let backgroundTable = mediaFiles[0]; + let backgroundCount = mediaFiles[1]; + if (backgroundCount > 0) { + let currentBg = backgroundTable[time() % backgroundCount]; + bgUrl = currentBg.url; + if (currentBg.is_video) { + backgroundType = 'Video'; + mimeType = 'video/' + currentBg.type; + } + } + + return [bgUrl, backgroundType, mimeType]; + } + + let themeDir = media + '/background/'; + let selectedBg = selectBackground(themeDir); + let bgUrl = selectedBg[0]; + let backgroundType = selectedBg[1]; + let mimeType = selectedBg[2]; +%} + + + + +
+
+ + +
+
+ + \ No newline at end of file diff --git a/luci-theme-luxe/template/header.htm b/luci-theme-luxe/template/header.htm new file mode 100644 index 000000000..afe897529 --- /dev/null +++ b/luci-theme-luxe/template/header.htm @@ -0,0 +1,193 @@ +<%# + Luxe is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI + + luci-theme-material + Copyright 2015-2017 Lutty Yang + + luci-theme-luxe + Copyright 2026 de-quenx | xidz_x + + Have a bug? Please create an issue here on GitHub! + https://github.com/de-quenx/luci-theme-luxe/issues + + luci-theme-bootstrap: + Copyright 2008 Steven Barth + Copyright 2008-2016 Jo-Philipp Wich + Copyright 2012 David Menting + + MUI: + https://github.com/muicss/mui + + Licensed to the public under the Apache License 2.0 +-%> + +<% + local sys = require "luci.sys" + local util = require "luci.util" + local http = require "luci.http" + local disp = require "luci.dispatcher" + local ver = require "luci.version" + + local boardinfo = util.ubus("system", "board") or { } + local hostname = striptags(boardinfo.hostname or "?") + + local node = disp.context.dispatched + local path = table.concat(disp.context.path, "-") + + http.prepare_content("text/html; charset=UTF-8") +-%> + + + + + + + + + + + + + + + +<% if node and node.css then %> + +<% end %> + + +<%=hostname%><% if node and node.title then %> - <%=striptags(node.title)%><% end %> - LuCI +<% if css then %> + +<% end %> + +node-<%= path %><% else %>node-main-login<% end %>" data-page="<%= pcdata(path) %>"> + +
+
+
+ +
+ + + +
+
+ Tampilkan Hostname + +
+
+ Floating Navbar + +
+
+ Sembunyi Otomatis Navbar + +
+
+ Slider Navbar + +
+
+ Jumlah Icon +
+
4
5
6
7
8
+
+
+
+ Gaya / Style +
+
LuxeOs
Glass Liquid
Glass Luxe
Glass Crystal
Glass Dark
Neobrutalism
Neomorphism
Neon
Rgb
Glaze
Onyx
Liquid Neo
+
+
+ +
+
+
+ +
+
+
+
+ + + +
+
<%:Collecting data...%>
+ +
+ +
+
+
+ <%- if luci.sys.process.info("uid") == 0 and luci.sys.user.getuser("root") and not luci.sys.user.getpasswd("root") and path ~= "admin-system-admin-password" then -%> +
+

<%:No password set!%>

+

<%:There is no password set on this router. Please configure a root password to protect the web interface.%>

+ <% if disp.lookup("admin/system/admin") then %> + + <% end %> +
+ <%- end -%> + + <% if boardinfo and boardinfo.rootfs_type == "initramfs" then %> +
+

<%:System running in recovery (initramfs) mode.%>

+

<%:No changes to settings will be stored and are lost after rebooting. This mode should only be used to install a firmware upgrade%>

+ <% if disp.lookup("admin/system/flash") then %> + + <% end %> +
+ <% end %> + + + + \ No newline at end of file diff --git a/luci-theme-luxe/template/header.ut b/luci-theme-luxe/template/header.ut new file mode 100644 index 000000000..a275ac1e9 --- /dev/null +++ b/luci-theme-luxe/template/header.ut @@ -0,0 +1,186 @@ +{# + Luxe is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI + + luci-theme-material + Copyright 2015-2017 Lutty Yang + + luci-theme-luxe + Copyright 2026 de-quenx | xidz_x + + Have a bug? Please create an issue here on GitHub! + https://github.com/de-quenx/luci-theme-luxe/issues + + luci-theme-bootstrap: + Copyright 2008 Steven Barth + Copyright 2008-2016 Jo-Philipp Wich + Copyright 2012 David Menting + + MUI: + https://github.com/muicss/mui + + Licensed to the public under the Apache License 2.0 +-#} + +{% + import { getuid, getspnam } from 'luci.core'; + + const boardinfo = ubus.call('system', 'board'); + const hostname = (boardinfo && boardinfo.hostname) ? striptags(boardinfo.hostname) : '?'; + + http.prepare_content('text/html; charset=UTF-8'); +-%} + + + + + + + + + + + + + + + +{% if (node && node.css): %} + +{% endif %} + + +{{ hostname }}{{ (node && node.title) ? ' - ' + striptags(node.title) : '' }} - LuCI +{% if (css): %} + +{% endif %} + + + +
+
+
+ +
+ + + +
+
+ Tampilkan Hostname + +
+
+ Floating Navbar + +
+
+ Sembunyi Otomatis Navbar + +
+
+ Slider Navbar + +
+
+ Jumlah Icon +
+
4
5
6
7
8
+
+
+
+ Gaya / Style +
+
LuxeOs
Glass Liquid
Glass Luxe
Glass Crystal
Glass Dark
Neobrutalism
Neomorphism
Neon
Rgb
Glaze
Onyx
Liquid Neo
+
+
+ +
+
+
+ +
+
+
+
+ + + +
+
{{ _('Collecting data...') }}
+ +
+ +
+
+
+ {% if (getuid() == 0 && getspnam('root') && getspnam('root').pwdp === '' && ctx.authsession): %} +
+

{{ _('No password set!') }}

+

{{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}

+ {% if (dispatcher.lookup("admin/system/admin")): %} + + {% endif %} +
+ {% endif %} + + {% if (boardinfo && boardinfo.rootfs_type == "initramfs"): %} +
+

{{ _('System running in recovery (initramfs) mode.') }}

+

{{ _('No changes to settings will be stored and are lost after rebooting. This mode should only be used to install a firmware upgrade') }}

+ {% if (dispatcher.lookup("admin/system/flash")): %} + + {% endif %} +
+ {% endif %} + + + + \ No newline at end of file diff --git a/luci-theme-luxe/template/sysauth.htm b/luci-theme-luxe/template/sysauth.htm new file mode 100644 index 000000000..d70583527 --- /dev/null +++ b/luci-theme-luxe/template/sysauth.htm @@ -0,0 +1,71 @@ +<%# + Luxe is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI + + luci-theme-material + Copyright 2015-2017 Lutty Yang + + luci-theme-luxe + Copyright 2026 de-quenx | xidz_x + + Have a bug? Please create an issue here on GitHub! + https://github.com/de-quenx/luci-theme-luxe/issues + + luci-theme-bootstrap: + Copyright 2008 Steven Barth + Copyright 2008-2016 Jo-Philipp Wich + Copyright 2012 David Menting + + MUI: + https://github.com/muicss/mui + + Licensed to the public under the Apache License 2.0 +-%> +<% + local util = require "luci.util" + local fs = require "nixio.fs" + + local boardinfo = util.ubus("system", "board") or { } + local hn_file = fs.readfile("/proc/sys/kernel/hostname") + local hostname = (hn_file and hn_file ~= "") and hn_file:gsub("\n", "") or (boardinfo.hostname or "OpenWrt") +-%> + + + + + + <%= hostname %> + + + + +
+ +
+
+ <% if fuser then %> +
+ Invalid username / password! Please try again. +
+ <% end %> + +
+ + + \ No newline at end of file diff --git a/luci-theme-luxe/template/sysauth.ut b/luci-theme-luxe/template/sysauth.ut new file mode 100644 index 000000000..32748bb34 --- /dev/null +++ b/luci-theme-luxe/template/sysauth.ut @@ -0,0 +1,75 @@ +{# + Luxe is a clean HTML5 theme for LuCI. It is based on luci-theme-bootstrap and MUI + + luci-theme-material + Copyright 2015-2017 Lutty Yang + + luci-theme-luxe + Copyright 2026 de-quenx | xidz_x + + Have a bug? Please create an issue here on GitHub! + https://github.com/de-quenx/luci-theme-luxe/issues + + luci-theme-bootstrap: + Copyright 2008 Steven Barth + Copyright 2008-2016 Jo-Philipp Wich + Copyright 2012 David Menting + + MUI: + https://github.com/muicss/mui + + Licensed to the public under the Apache License 2.0 +-#} + +{% + let fs = require("fs"); + let binfo = (type(boardinfo) == 'object') ? boardinfo : {}; + let hn_file = fs.readfile("/proc/sys/kernel/hostname"); + let hostname = (type(hn_file) == 'string' && length(hn_file) > 0) ? replace(hn_file, "\n", "") : (binfo.hostname || "OpenWrt"); + let e = (type(env) == 'object') ? env : {}; + let req_uri = e.REQUEST_URI || ''; + let user = (type(duser) == 'string') ? duser : ''; + let is_failed = (type(fuser) != 'undefined' && fuser) ? true : false; + let m = (type(media) == 'string') ? media : ''; +-%} + + + + + + {{ hostname }} + + + + +
+ +
+
+ {% if (is_failed): %} +
+ Invalid username / password! Please try again. +
+ {% endif %} + +
+ + + \ No newline at end of file diff --git a/luci-theme-noobwrt/.gitignore b/luci-theme-noobwrt/.gitignore new file mode 100644 index 000000000..7b797458a --- /dev/null +++ b/luci-theme-noobwrt/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +node_modules +settings.local.json +.claude +./package-lock.json +./yarn.lock +./package.json +./.env +./.env.local +./.env.development +./.env.production +./.env.test +./.env.*.local +./.vscode +./.idea diff --git a/luci-theme-noobwrt/LICENSE b/luci-theme-noobwrt/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/luci-theme-noobwrt/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/luci-theme-noobwrt/Makefile b/luci-theme-noobwrt/Makefile new file mode 100644 index 000000000..eb03f3ede --- /dev/null +++ b/luci-theme-noobwrt/Makefile @@ -0,0 +1,18 @@ +# +# Copyright (C) 2025 NoobLk +# +# This is free software, licensed under the Apache License, Version 2.0 . +# + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=Noobwrt Theme +LUCI_DEPENDS:=+wget +jsonfilter +PKG_VERSION:=1.1.7 +PKG_RELEASE:=1 + +CONFIG_LUCI_CSSTIDY:=1 + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-theme-noobwrt/README.md b/luci-theme-noobwrt/README.md new file mode 100644 index 000000000..f37c924b7 --- /dev/null +++ b/luci-theme-noobwrt/README.md @@ -0,0 +1,187 @@ + + +[license]: /LICENSE +[license-badge]: https://img.shields.io/github/license/nooblk-98/luci-theme-noobwrt?style=flat-square&a=1 +[prs]: https://github.com/nooblk-98/luci-theme-noobwrt/pulls +[prs-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square +[issues]: https://github.com/nooblk-98/luci-theme-noobwrt/issues/new +[issues-badge]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=flat-square +[release]: https://github.com/nooblk-98/luci-theme-noobwrt/releases +[release-badge]: https://img.shields.io/github/v/release/nooblk-98/luci-theme-noobwrt?style=flat-square +[download]: https://github.com/nooblk-98/luci-theme-noobwrt/releases +[download-badge]: https://img.shields.io/github/downloads/nooblk-98/luci-theme-noobwrt/total?style=flat-square +[official]: https://github.com/openwrt/openwrt +[immortalwrt]: https://github.com/immortalwrt/immortalwrt + + +# NoobWRT LuCI Theme + +> A clean, modern OpenWrt LuCI theme with live wallpapers, animated progress bars, automatic light/dark scheduling, customizable accent colors, and a quick-access toolbar. + +

+ + license + + + prs + + + issues + + + release + + + download + +

+ +
+ +

+ NoobWRT Login Page +

+ +--- + +## Screenshots + +### Desktop + + + + + + + + + +
+ Overview — Light Mode +
Overview — Light Mode +
+ Overview — Dark Mode +
Overview — Dark Mode +
+ Theme Settings — Toolbar Panel +
Theme Settings — Toolbar Panel +
+ +### Mobile + + + + + + + +
+ Mobile — Status (Light) +
Mobile — Status (Light) +
+ Mobile — Status (Dark) +
Mobile — Status (Dark) +
+ Mobile — Login +
Mobile — Login +
+ +--- + +## Features + +- **Animated progress bars** — diagonal stripe animation using your accent color +- **Online wallpapers** — Bing Daily Photo, Lorem Picsum, or Wikimedia Picture of the Day, no API key required +- **Custom wallpaper upload** — upload your own JPG/PNG/WebP login background +- **Accent color picker** — separate colors for light and dark mode, applied everywhere +- **Automatic Light / Dark scheduling** — switches to Dark from 6 PM to 6 AM and Light from 6 AM to 6 PM automatically; enable/disable in settings (default: enabled) +- **Light / Dark mode** — manual toggle via toolbar sun/moon button; respects OS preference when auto-schedule is off +- **Quick-access toolbar** — slides in from the right edge; hover the `‹` tab to peek, click to pin open/closed +- **Blur & opacity controls** — fine-tune the login page background effect +- **Responsive** — works on desktop and mobile + +--- + +## Theme Settings + +Go to **System → NoobWrt Theme** in the LuCI menu. + +### Appearance + +| Option | Description | +|--------|-------------| +| **Automatic Light / Dark Mode** | When enabled (default), the theme switches to Dark from **6 PM to 6 AM** and to Light from **6 AM to 6 PM** automatically. The toolbar toggle still works but the schedule takes effect on the next minute tick. Disable to use manual mode. | +| **Default Theme Mode** | `Light` or `Dark` — applied on page load when Automatic mode is disabled. Toggle anytime via the toolbar sun/moon button. | +| **Accent Color — Light Mode** | Hex color for the light theme, e.g. `#5e72e4`. Used for progress bars, buttons, active states. | +| **Accent Color — Dark Mode** | Hex color for the dark theme, e.g. `#7c8ff5`. | + +### Wallpaper + +| Action | Description | +|--------|-------------| +| **Online Wallpaper Source** | `Bing Daily Photo`, `Lorem Picsum (Random)`, or `Wikimedia Picture of the Day`. Set to `None` to use a custom upload or the default. | +| **Upload Wallpaper** | Upload a JPG, PNG, or WebP image (max 10 MB) as the login background. | +| **Revert to Default** | Remove the custom wallpaper and restore the built-in default. | + +> Online sources are cached for 12 hours. Delete `/var/run/noobwrt_*.url` on the router to force a refresh. + +### Background Effects + +| Option | Description | +|--------|-------------| +| **Blur Radius — Light Mode** | Background blur in pixels, light mode. Default: `10` | +| **Blur Radius — Dark Mode** | Background blur in pixels, dark mode. Default: `10` | +| **Background Opacity — Light Mode** | Overlay opacity, `0.0` (transparent) to `1.0` (opaque). Default: `0.8` | +| **Background Opacity — Dark Mode** | Overlay opacity, dark mode. Default: `0.8` | + +### Toolbar Panel + +The toolbar is a quick-access icon panel fixed to the right edge of every page. + +**Behaviour:** +- A `‹` tab is always visible on the right edge. **Hover** it to slide the panel in temporarily — move the mouse away and it slides back. +- **Click** the tab to pin the panel open or closed. The state is saved across page loads. + +| Field | Description | +|-------|-------------| +| **Enable** | Show or hide this item. | +| **Label** | Tooltip text shown on hover. | +| **URL / Path** | Destination URL, e.g. `/cgi-bin/luci/admin/status/overview`. | +| **Icon** | Choose from: Home, Signal, Cell, SMS, Network, NAS, WiFi, Firewall, Settings, Terminal, VPN, Files, Info. | +| **Order** | Position in the toolbar — lower numbers appear first. | + +--- + +## Installation + +### Install via opkg + +Download the latest `.ipk` from [Releases](https://github.com/nooblk-98/luci-theme-noobwrt/releases) and run: + +```bash +opkg install luci-theme-noobwrt_*.ipk +``` + +### Build from source + +```bash +cd openwrt/package +git clone https://github.com/nooblk-98/luci-theme-noobwrt.git +make menuconfig # LUCI → Themes → luci-theme-noobwrt +make -j1 V=s +``` + +--- + +## Credits + +- [luci-theme-argon](https://github.com/jerrykuku/luci-theme-argon/) — original inspiration +- [luci-theme-material](https://github.com/LuttyYang/luci-theme-material/) diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/background/README.md b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/background/README.md new file mode 100644 index 000000000..a86f27b06 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/background/README.md @@ -0,0 +1,2 @@ +Drop background here! +accept jpg png gif mp4 webm diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/css/cascade.css b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/css/cascade.css new file mode 100644 index 000000000..c4f94745b --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/css/cascade.css @@ -0,0 +1,4571 @@ +/** + * NoobWrt is a clean HTML5 theme for LuCI. It is based on luci-theme-material and NoobWrt Template + * + * luci-theme-noobwrt + * Copyright 2025 Jerrykuku + * + * Have a bug? Please create an issue here on GitHub! + * https://github.com/jerrykuku/luci-theme-noobwrt/issues + * + * luci-theme-material: + * Copyright 2015 Lutty Yang + * https://github.com/LuttyYang/luci-theme-material/ + * + * Agron Theme + * https://demos.creative-tim.com/argon-dashboard/index.html + * + * Login background + * https://unsplash.com/ + * Font generate by Icomoon + * + * Licensed to the public under the Apache License 2.0 + */ +:root { + --primary: #5e72e4; + --dark-primary: #483d8b; + --header-color: #fff; + --menu-bg-color: #ffffff; + --menu-color: #5f6368; + --main-menu-color: #202124; + --red: #f5365c; + --orange: #fb6340; + --yellow: #ffd600; + --green: #2dce89; + --teal: #11cdef; + --cyan: #2bffc6; + --white: #fff; + --gray: #8898aa; + --gray-dark: #32325d; + --light: #adb5bd; + --lighter: #e9ecef; + --success: #2dce89; + --info: #11cdef; + --warning: #fb6340; + --danger: #f5365c; + --dark: #212529; + --default: #172b4d; + --darker: black; + --background-color: #f4f5f7; + --login-form-bg-color: rgba(244, 245, 247, 0.8); + --blur-radius: 10px; + --blur-opacity: 0.5; + --blur-radius-dark: 10px; + --blur-opacity-dark: 0.5; + --font-family-sans-serif: "Red Hat Display", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; + --dropdown-arrow-icon: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik01LjI5MyA5LjI5M2ExIDEgMCAwIDEgMS40MTQgMEwxMiAxNC41ODZsNS4yOTMtNS4yOTNhMSAxIDAgMSAxIDEuNDE0IDEuNDE0bC02IDZhMSAxIDAgMCAxLTEuNDE0IDBsLTYtNmExIDEgMCAwIDEgMC0xLjQxNCIvPjwvc3ZnPg=="); +} +/* + * Include base and custom css + */ +@font-face { + font-family: 'Red Hat Display'; + src: url('../fonts/RedHatDisplay-Variable.woff2') format('woff2 supports variations'), url('../fonts/RedHatDisplay-Variable.woff2') format('woff2'); + font-weight: 300 900; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Red Hat Display'; + src: url('../fonts/RedHatDisplay-Italic-Variable.woff2') format('woff2 supports variations'), url('../fonts/RedHatDisplay-Italic-Variable.woff2') format('woff2'); + font-weight: 300 900; + font-style: italic; + font-display: swap; +} +/* Logo Font */ +@font-face { + font-family: 'TypoGraphica'; + src: local('TypoGraphica'), url('../fonts/TypoGraphica.woff2') format('woff2'), url('../fonts/TypoGraphica.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} +/* Icon Font */ +@font-face { + font-family: 'noobwrt'; + src: url('../fonts/noobwrt.woff2') format('woff2'), url('../fonts/noobwrt.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} +html { + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +html, +body { + margin: 0; + padding: 0; + height: 100%; + font-family: var(--font-family-sans-serif); +} +body { + background-color: var(--background-color); + color: var(--gray-dark); + -webkit-tap-highlight-color: transparent; + font-size: 0.875rem; +} +main { + display: block; +} +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: normal; + line-height: 1.1 !important; + color: inherit; +} +h1 { + font-size: 2rem; + margin: 0.67em 0; + padding-bottom: 0.5rem; + border-bottom: thin solid var(--lighter); +} +h2 { + padding: 1rem 1.25rem; + font-size: 1.25rem; + font-weight: bold; + color: var(--gray-dark); + border-radius: 0.25rem; + background: var(--white); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); +} +h3 { + margin: 0; + padding: 0.8755rem 1.25rem; + font-size: 1.1rem; + font-weight: bold; + line-height: 1; + color: var(--gray-dark); + border-radius: 0.25rem; + background: var(--white); + display: block; + width: 100%; +} +h4 { + margin: 0; + padding: 0.75rem 1.25rem; + font-size: 0.875rem; + font-weight: bold; + color: var(--gray-dark-400); +} +h4 em { + padding: 0 0.5rem; +} +h5 { + margin: 2rem 0 0 0; + padding-bottom: 0.5rem; + font-size: 1rem; +} +a { + background-color: transparent; +} +abbr { + cursor: help; + text-decoration: underline; + color: var(--primary); +} +abbr[title] { + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} +b, +strong { + font-weight: bolder; +} +code, +kbd, +samp { + font-family: inherit; + font-size: inherit; + padding: 1px 3px; + color: var(--dark); + border-radius: 0.5rem; + background: var(--lighter); +} +small { + font-size: 90%; + line-height: 1.42857143; + white-space: normal; +} +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} +sub { + bottom: -0.25em; +} +sup { + top: -0.5em; +} +hr { + box-sizing: content-box; + height: 0; + margin: 1rem 0; + overflow: visible; + opacity: 0.1; + border-color: var(--lighter); +} +pre { + font-family: monospace, monospace; + font-size: 1em; + overflow: auto; +} +img { + border-style: none; +} +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; +} +button, +input { + overflow: visible; +} +button, +select { + text-transform: none; +} +[type=button], +[type=reset], +[type=submit], +button { + -webkit-appearance: button; + appearance: button; +} +[type=button]::-moz-focus-inner, +[type=reset]::-moz-focus-inner, +[type=submit]::-moz-focus-inner, +button::-moz-focus-inner { + border-style: none; + padding: 0; +} +[type=button]:-moz-focusring, +[type=reset]:-moz-focusring, +[type=submit]:-moz-focusring, +button:-moz-focusring { + outline: 1px dotted ButtonText; +} +[type=checkbox], +[type=radio] { + box-sizing: border-box; + padding: 0; +} +[type=number]::-webkit-inner-spin-button, +[type=number]::-webkit-outer-spin-button { + height: auto; +} +[type=search] { + -webkit-appearance: textfield; + appearance: textfield; + outline-offset: -2px; +} +[type=search]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} +::-webkit-file-upload-button { + -webkit-appearance: button; + appearance: button; + font: inherit; +} +input[type="checkbox"] { + appearance: none !important; + -webkit-appearance: none !important; + border: 1px solid var(--primary); + width: 1rem !important; + height: 1rem !important; + padding: 0; + cursor: pointer; + transition: all 0.2s; +} +input[type="checkbox"]:checked { + border: 1px solid var(--primary); + background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 8 8\'%3e%3cpath fill=\'%23fff\' d=\'M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z\'/%3e%3c/svg%3e') !important; + background-color: var(--primary); + background-size: 70%; + background-repeat: no-repeat; + background-position: center; +} +select { + padding: 0.36rem 0.8rem; + color: var(--gray-dark); + border: thin solid var(--lighter); + background-color: var(--white); + background-image: none; +} +textarea { + padding: 0.25rem; + overflow: auto; +} +textarea:focus-visible { + outline: none; + border: 1px solid var(--primary); +} +fieldset { + padding: 0.35em 0.75em 0.625em; +} +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; +} +details { + display: block; +} +summary { + display: list-item; +} +template { + display: none; +} +progress { + vertical-align: baseline; +} +ul { + line-height: normal; +} +li { + list-style-type: none; +} +[disabled="disabled"] { + pointer-events: none; +} +::selection { + background-color: var(--primary); + color: var(--white); +} +::placeholder { + color: var(--lighter); +} +a:link, +a:visited, +a:active { + color: var(--primary); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.nowrap:not(.td) { + white-space: nowrap; +} +div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] { + border: 0 !important; +} +/** +Pure v3.0.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/** +normalize.css v | MIT License | https://necolas.github.io/normalize.css/ +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/** normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ +.pure-g { + display: flex; + flex-flow: row wrap; + align-content: flex-start; +} +.pure-u { + display: inline-block; + vertical-align: top; +} +.pure-u-1, +.pure-u-1-1, +.pure-u-1-12, +.pure-u-1-2, +.pure-u-1-24, +.pure-u-1-3, +.pure-u-1-4, +.pure-u-1-5, +.pure-u-1-6, +.pure-u-1-8, +.pure-u-10-24, +.pure-u-11-12, +.pure-u-11-24, +.pure-u-12-24, +.pure-u-13-24, +.pure-u-14-24, +.pure-u-15-24, +.pure-u-16-24, +.pure-u-17-24, +.pure-u-18-24, +.pure-u-19-24, +.pure-u-2-24, +.pure-u-2-3, +.pure-u-2-5, +.pure-u-20-24, +.pure-u-21-24, +.pure-u-22-24, +.pure-u-23-24, +.pure-u-24-24, +.pure-u-3-24, +.pure-u-3-4, +.pure-u-3-5, +.pure-u-3-8, +.pure-u-4-24, +.pure-u-4-5, +.pure-u-5-12, +.pure-u-5-24, +.pure-u-5-5, +.pure-u-5-6, +.pure-u-5-8, +.pure-u-6-24, +.pure-u-7-12, +.pure-u-7-24, +.pure-u-7-8, +.pure-u-8-24, +.pure-u-9-24 { + display: inline-block; + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto; +} +.pure-u-1-24 { + width: 4.1667%; +} +.pure-u-1-12, +.pure-u-2-24 { + width: 8.3333%; +} +.pure-u-1-8, +.pure-u-3-24 { + width: 12.5%; +} +.pure-u-1-6, +.pure-u-4-24 { + width: 16.6667%; +} +.pure-u-1-5 { + width: 20%; +} +.pure-u-5-24 { + width: 20.8333%; +} +.pure-u-1-4, +.pure-u-6-24 { + width: 25%; +} +.pure-u-7-24 { + width: 29.1667%; +} +.pure-u-1-3, +.pure-u-8-24 { + width: 33.3333%; +} +.pure-u-3-8, +.pure-u-9-24 { + width: 37.5%; +} +.pure-u-2-5 { + width: 40%; +} +.pure-u-10-24, +.pure-u-5-12 { + width: 41.6667%; +} +.pure-u-11-24 { + width: 45.8333%; +} +.pure-u-1-2, +.pure-u-12-24 { + width: 50%; +} +.pure-u-13-24 { + width: 54.1667%; +} +.pure-u-14-24, +.pure-u-7-12 { + width: 58.3333%; +} +.pure-u-3-5 { + width: 60%; +} +.pure-u-15-24, +.pure-u-5-8 { + width: 62.5%; +} +.pure-u-16-24, +.pure-u-2-3 { + width: 66.6667%; +} +.pure-u-17-24 { + width: 70.8333%; +} +.pure-u-18-24, +.pure-u-3-4 { + width: 75%; +} +.pure-u-19-24 { + width: 79.1667%; +} +.pure-u-4-5 { + width: 80%; +} +.pure-u-20-24, +.pure-u-5-6 { + width: 83.3333%; +} +.pure-u-21-24, +.pure-u-7-8 { + width: 87.5%; +} +.pure-u-11-12, +.pure-u-22-24 { + width: 91.6667%; +} +.pure-u-23-24 { + width: 95.8333%; +} +.pure-u-1, +.pure-u-1-1, +.pure-u-24-24, +.pure-u-5-5 { + width: 100%; +} +.col-1 { + flex: 1 1 30px !important; +} +.col-2 { + flex: 2 2 60px !important; +} +.col-3 { + flex: 3 3 90px !important; +} +.col-4 { + flex: 4 4 120px !important; +} +.col-5 { + flex: 5 5 150px !important; +} +.col-6 { + flex: 6 6 180px !important; +} +.col-7 { + flex: 7 7 210px !important; +} +.col-8 { + flex: 8 8 240px !important; +} +.col-9 { + flex: 9 9 270px !important; +} +.col-10 { + flex: 10 10 300px !important; +} +.table { + position: relative; + display: table; +} +.tr { + display: table-row; +} +.thead { + display: table-header-group; +} +.tbody { + display: table-row-group; +} +.tfoot { + display: table-footer-group; +} +.td, +.th { + line-height: normal; + display: table-cell; + padding: 0.5em; + text-align: center; + vertical-align: middle; +} +.th { + font-weight: bold; + white-space: nowrap; +} +.tr.placeholder { + height: 4em; +} +.tr.placeholder > .td { + line-height: 3; + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 0.4rem 0 !important; + text-align: center !important; + background: inherit; +} +.td[width="33%"] { + padding: 1.1em 1.5rem; +} +.table[width="33%"], +.th[width="33%"], +.td[width="33%"] { + width: 33%; +} +.table[width="100%"], +.th[width="100%"], +.td[width="100%"] { + width: 100%; +} +.btn, +button, +select, +input, +.cbi-dropdown { + line-height: 1.5rem; + height: 2.5rem; + padding: 0.5rem 0.75rem; + color: #8898aa; + border: 1px solid #dee2e6; + border-radius: 0.25rem; + outline: 0; + background-image: none; + box-shadow: none; + transition: box-shadow 0.15s ease; +} +select { + padding-right: 1.5rem; + background-image: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjODg5OGFhIiBkPSJNNS4yOTMgOS4yOTNhMSAxIDAgMCAxIDEuNDE0IDBMMTIgMTQuNTg2bDUuMjkzLTUuMjkzYTEgMSAwIDEgMSAxLjQxNCAxLjQxNGwtNiA2YTEgMSAwIDAgMS0xLjQxNCAwbC02LTZhMSAxIDAgMCAxIDAtMS40MTQiLz48L3N2Zz4="); + background-size: 1rem; + background-position: right 0.5rem center; + background-repeat: no-repeat; + appearance: none; +} +select:not([multiple="multiple"]):focus, +input:not(.cbi-button):focus, +.cbi-dropdown:focus { + border-color: var(--primary); + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.cbi-dropdown, +select[multiple="multiple"] { + height: auto; +} +.login-page { + position: relative; + display: flex; + min-height: 100vh; + flex-direction: column; + font-family: 'Google Sans', 'Segoe UI', Tahoma, sans-serif; + color: var(--default); +} +.login-page::before { + content: none; +} +.login-page .video { + position: absolute; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--darker); + overflow: hidden; + z-index: 0; +} +.login-page .video video { + width: 100%; + height: auto; +} +.login-page .volume-control { + position: fixed; + top: 1rem; + right: 1rem; + width: 1.5rem; + height: 1.5rem; + z-index: 5000; + cursor: pointer; + background: url(../img/volume_high.svg) no-repeat center / contain; +} +.login-page .volume-control.mute { + background-image: url(../img/volume_off.svg); +} +.login-page .main-bg { + position: absolute; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + background: url(../img/blank.png) no-repeat center / cover; + transition: all 0.5s ease; + z-index: 0; +} +.login-page .login-container { + z-index: 2; + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + width: 100%; + padding: 2rem 1.5rem; +} +.login-page .login-container .login-form { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 46rem; +} +.login-page .login-container .login-form .login-card { + width: 100%; + padding: 0; + border-radius: 1.5rem; + background: rgba(255, 255, 255, 0.98); + border: 1px solid rgba(255, 255, 255, 0.6); + backdrop-filter: blur(10px) saturate(120%); + box-shadow: 0 30px 80px rgba(24, 14, 84, 0.45); + overflow: hidden; +} +.login-page .login-container .login-form .login-card.split { + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 22rem; +} +.login-page .login-container .login-form .login-card .login-hero { + position: relative; + padding: 2.5rem; + background: linear-gradient(135deg, rgba(244, 245, 255, 0.95), rgba(233, 236, 255, 0.95)); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles { + position: absolute; + inset: 0; + pointer-events: none; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(109, 93, 246, 0.38); + box-shadow: 0 0 18px rgba(109, 93, 246, 0.4); + animation: floaty 7s ease-in-out infinite; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(1) { + top: 18%; + left: 18%; + width: 6px; + height: 6px; + animation-delay: 0s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(2) { + top: 26%; + right: 22%; + width: 12px; + height: 12px; + animation-delay: 1s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(3) { + bottom: 30%; + left: 28%; + width: 7px; + height: 7px; + animation-delay: 2s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(4) { + bottom: 20%; + right: 30%; + width: 9px; + height: 9px; + animation-delay: 1.5s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(5) { + top: 55%; + left: 12%; + width: 5px; + height: 5px; + animation-delay: 2.8s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(6) { + top: 60%; + right: 15%; + width: 6px; + height: 6px; + animation-delay: 3.2s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(7) { + top: 10%; + right: 45%; + width: 4px; + height: 4px; + animation-delay: 1.8s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(8) { + bottom: 12%; + left: 45%; + width: 8px; + height: 8px; + animation-delay: 2.4s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(9) { + top: 42%; + right: 8%; + width: 5px; + height: 5px; + animation-delay: 3.6s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(10) { + top: 70%; + left: 6%; + width: 7px; + height: 7px; + animation-delay: 4.2s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(11) { + top: 34%; + left: 60%; + width: 6px; + height: 6px; + animation-delay: 1.2s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(12) { + bottom: 40%; + right: 40%; + width: 10px; + height: 10px; + animation-delay: 2.9s; +} +.login-page .login-container .login-form .login-card .login-hero .hero-icon { + width: 9.5rem; + height: 9.5rem; + border-radius: 50%; + background: rgba(109, 93, 246, 0.08); + border: 1px solid rgba(109, 93, 246, 0.25); + box-shadow: 0 0 28px rgba(109, 93, 246, 0.25), inset 0 0 18px rgba(109, 93, 246, 0.12); + display: flex; + align-items: center; + justify-content: center; +} +.login-page .login-container .login-form .login-card .login-hero .hero-icon img { + width: 6.3rem; + height: 6.3rem; +} +.login-page .login-container .login-form .login-card .login-hero .hero-text { + text-align: center; +} +.login-page .login-container .login-form .login-card .login-hero .hero-brand { + max-width: 12rem; + height: auto; + display: inline-block; +} +.login-page .login-container .login-form .login-card .login-panel { + padding: 2.5rem 2.5rem 2.25rem; + display: flex; + flex-direction: column; + justify-content: center; +} +.login-page .login-container .login-form .brand-text-only { + text-align: center; + font-size: 1.5rem; + font-weight: 800; + font-family: "Google Sans"; + color: #1f2a44; + letter-spacing: 0.02rem; + margin: 0 auto 0.3rem; +} +.login-page .login-container .login-form .login-subtitle { + text-align: center; + color: #7b849c; + font-size: 0.9rem; + font-weight: 500; + margin-bottom: 1.5rem; +} +.login-page .login-container .login-form .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + white-space: nowrap; +} +.login-page .login-container .login-form .form-login { + width: 100%; + padding: 0; + box-sizing: border-box; +} +.login-page .login-container .login-form .form-login .errorbox { + margin-bottom: 1.25rem; + padding: 0.75rem 1rem; + border-radius: 0.9rem; + text-align: center; + color: #b42318; + background: rgba(255, 87, 87, 0.12); + border: 1px solid rgba(255, 87, 87, 0.3); +} +.login-page .login-container .login-form .form-login .input-group { + position: relative; + margin-bottom: 1.1rem; + display: flex; + align-items: center; +} +.login-page .login-container .login-form .form-login .input-group::before { + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + position: absolute; + top: 50%; + left: 1.1rem; + z-index: 100; + font-size: 1.25rem; + color: #6b7280; + transform: translateY(-50%); +} +.login-page .login-container .login-form .form-login .input-group input { + display: block; + width: 100%; + padding: 0.9rem 1rem 0.9rem 3.2rem; + font-size: 0.95rem; + line-height: 1.5em; + color: #1f2937; + background-color: #f6f7fb; + background-clip: padding-box; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 999px; + outline: none; + box-sizing: border-box; + box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06), 0 8px 18px rgba(15, 23, 42, 0.08); + transition: all 0.25s ease; +} +.login-page .login-container .login-form .form-login .input-group input:focus { + border-color: rgba(109, 93, 246, 0.6); + background-color: #ffffff; + box-shadow: 0 0 0 4px rgba(109, 93, 246, 0.15); +} +.login-page .login-container .login-form .form-login .input-group input::placeholder { + color: #8b93a7; + letter-spacing: 0.01rem; +} +.login-page .login-container .login-form .form-login .input-group.user-icon::before { + content: "\e971"; +} +.login-page .login-container .login-form .form-login .input-group.pass-icon::before { + content: "\e910"; +} +.login-page .login-container .login-form .cbi-button-apply { + width: 100% !important; + min-height: 3rem; + margin: 1.2rem 0 0.2rem; + padding: 0.85rem 0; + font-size: 0.95rem; + font-weight: 600; + color: var(--white); + text-align: center; + letter-spacing: 0.08rem; + background: linear-gradient(135deg, #6d5df6, #4d8bff) !important; + border: none; + border-radius: 0.9rem; + outline: none; + cursor: pointer; + box-shadow: 0 16px 30px rgba(84, 88, 245, 0.35); + transition: all 0.3s ease !important; +} +.login-page .login-container .login-form .cbi-button-apply:hover, +.login-page .login-container .login-form .cbi-button-apply:focus { + transform: translateY(-1px); + box-shadow: 0 22px 40px rgba(84, 88, 245, 0.45); +} +.login-page .login-container footer { + position: absolute; + bottom: 0; + z-index: 10; + display: flex; + justify-content: space-evenly; + width: 100%; + margin-top: auto; + padding: 0 0 30px; + color: var(--gray); + text-align: center; + line-height: 1.6rem; + font-size: 0.75rem; +} +.login-page .login-container footer .ftc { + position: absolute; + bottom: 30px; + width: 100%; +} +.login-page .login-container footer .luci-link { + display: block; +} +@keyframes floaty { + 0% { + transform: translateY(0) scale(1); + opacity: 0.45; + } + 50% { + transform: translateY(-16px) scale(1.2); + opacity: 1; + } + 100% { + transform: translateY(0) scale(1); + opacity: 0.45; + } +} +.main { + position: relative; + width: 100%; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: row; +} +.main-left { + width: 15rem; + height: 100%; + flex-shrink: 0; + z-index: 100; + overflow-x: auto; + word-break: break-word; + background-color: var(--menu-bg-color); + box-shadow: rgba(0, 0, 0, 0.75) 0px 0px 15px -5px; + transition: all 0.2s; + scrollbar-width: thin; + scrollbar-color: transparent transparent; +} +.main-left .sidenav-header { + padding: 1.5rem 0.5rem; + text-align: center; +} +.main-left .sidenav-header .brand { + display: block; + margin: 0 2rem; + font-size: 1.8rem; + font-family: "TypoGraphica"; + color: var(--primary); + text-decoration: none; + text-align: center; + cursor: default; +} +.main-left .sidenav-header .brand .logo { + max-width: 100%; + height: auto; +} +.main-left .nav { + margin-top: 0.5rem; +} +.main-left .nav > li > a:first-child { + display: block; + position: relative; + margin: 0.1rem 0.5rem; + padding: 0.675rem 0 0.675rem 2.5rem; + font-size: 1rem; + text-decoration: none; + border-radius: 0.25rem; + cursor: default; + transition: all 0.2s; +} +.main-left .nav > li > a:first-child.active { + color: #fff; + background: var(--primary); +} +.main-left .nav > li > a:first-child.active::before { + color: #fff !important; +} +.main-left .nav > li > a:first-child.active::after { + transform: rotate(90deg); + color: #fff !important; +} +.main-left .nav > li > a:first-child:hover { + cursor: pointer; + color: #fff; + background: var(--primary); +} +.main-left .nav > li > a:first-child:hover::before { + color: #fff !important; +} +.main-left .nav > li > a:first-child::before { + position: absolute; + left: 0.8rem; + padding-top: 3px; + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e915"; + color: var(--primary); + transition: all 0.3s; +} +.main-left .nav > .slide > .menu::before { + transition: transform 0.1s ease-in-out; +} +.main-left .nav > .slide > .menu.active::before { + transition: transform 0.2s ease-in-out; +} +.main-left .nav li { + padding: 0; + cursor: pointer; + user-select: none; +} +.main-left .nav li a { + display: block; + color: var(--menu-color); +} +.main-left .nav li.slide { + padding: 0; +} +.main-left .nav li.slide ul { + display: none; + overflow: hidden; +} +.main-left .nav li.slide:hover { + background: none; +} +.main-left .nav li.slide .slide-menu { + margin: 0 0.5rem 0 2.5rem; + padding: 0rem 0.5rem; +} +.main-left .nav li.slide .slide-menu.active { + display: block; +} +.main-left .nav li.slide .slide-menu li { + position: relative; + margin: 0; + border-radius: 0.25rem; + background: none; + list-style: none; +} +.main-left .nav li.slide .slide-menu li a { + padding: 0.5rem 0rem; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.main-left .nav li.slide .slide-menu li::after { + position: absolute; + left: 0; + bottom: 0; + width: 0; + height: 2px; + content: ""; + background-color: var(--primary); + transition: all 0.2s; +} +.main-left .nav li.slide .slide-menu li:hover { + background: none; +} +.main-left .nav li.slide .slide-menu li:hover::after { + width: 100%; +} +.main-left .nav li.slide .slide-menu .active { + background: none; + color: var(--menu-color); +} +.main-left .nav li.slide .slide-menu .active a { + color: var(--menu-color); +} +.main-left .nav li.slide .slide-menu .active::after { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 2px; + content: ""; + background-color: var(--primary); + transition: all 0.2s; +} +.main-left .nav li.slide .slide-menu .active:hover { + background: none; +} +.main-left .nav li.slide .slide-menu .active:hover::after { + width: 100%; +} +.main-left .nav li .menu { + display: block; + position: relative; + margin: 0.1rem 0.5rem; + padding: 0.675rem 0 0.675rem 2.5rem; + font-size: 1rem; + text-decoration: none; + border-radius: 0.25rem; + cursor: default; + transition: all 0.2s; +} +.main-left .nav li .menu.active { + color: #fff; + background: var(--primary); +} +.main-left .nav li .menu.active::before { + color: #fff !important; +} +.main-left .nav li .menu.active::after { + transform: rotate(90deg); + color: #fff !important; +} +.main-left .nav li .menu:hover { + cursor: pointer; + color: #fff; + background: var(--primary); +} +.main-left .nav li .menu:hover::before { + color: #fff !important; +} +.main-left .nav li .menu::before { + position: absolute; + left: 0.8rem; + padding-top: 3px; + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e915"; + color: var(--primary); + transition: all 0.3s; +} +.main-left .nav li .menu::after { + position: absolute; + right: 0.5rem; + top: 0.8rem; + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: '\e90f'; + color: #ced4da; + text-rendering: auto; + transition: all 0.3s; +} +.main-left .nav li .menu[data-title="Status"]:before { + content: "\e906"; + color: var(--primary); +} +.main-left .nav li .menu[data-title="System"]:before { + content: "\e90a"; + color: #fb6340; +} +.main-left .nav li .menu[data-title="Services"]:before { + content: "\e909"; + color: #11cdef; +} +.main-left .nav li .menu[data-title="NAS"]:before { + content: "\e90c"; + color: #f3a4b5; +} +.main-left .nav li .menu[data-title="VPN"]:before { + content: "\e90b"; + color: #8965e0; +} +.main-left .nav li .menu[data-title="Network"]:before { + content: "\e908"; + color: #8965e0; +} +.main-left .nav li .menu[data-title="Bandwidth_Monitor"]:before { + content: "\e90d"; + color: #2dce89; +} +.main-left .nav li .menu[data-title="Docker"]:before { + content: "\e911"; + color: #6699ff; +} +.main-left .nav li .menu[data-title="Statistics"]:before { + content: "\e913"; + color: #8965e0; +} +.main-left .nav li .menu[data-title="Control"]:before { + content: "\e912"; + color: var(--primary); +} +.main-left .nav li .menu[data-title="Asterisk"]:before { + content: "\e914"; + color: #fb6340; +} +.main-left .nav li a[data-title="Log_out"]::before, +.main-left .nav li .food[data-title="Log_out"]::before { + content: "\e907"; + color: #adb5bd; +} +.main-left[style*="overflow: hidden"] > .nav > .slide > .menu::before { + display: none; +} +.main-left::-webkit-scrollbar { + width: 4px; + height: 1px; +} +.main-left::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: 4px; + transition: background-color 0.2s; +} +.main-left::-webkit-scrollbar-track { + background-color: transparent; +} +.main-left:hover::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.18); +} +.main-left:hover { + scrollbar-color: rgba(0, 0, 0, 0.18) transparent; +} +.main-right { + height: 100%; + flex-grow: 1; + overflow-x: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; + transition: all 0.2s; +} +.main-right > #maincontent { + position: relative; + z-index: 50; + flex: 1; + display: flex; + flex-direction: column; +} +.main-right > #maincontent > .container { + margin: 0 1.25rem 1rem 1.25rem; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 1rem; +} +.main-right > #maincontent .Dashboard { + color: var(--gray-dark) !important; +} +.main-right > #maincontent .Dashboard h3 { + color: var(--gray-dark); +} +.main-right > #maincontent .Dashboard p { + margin-top: 3px; + margin-bottom: 3px; +} +.main-right > #maincontent .Dashboard hr { + border-top: 1px solid #000000; +} +.main-right > #maincontent .Dashboard .dashboard-bg { + background-color: #fff; +} +.main-right > #maincontent .Dashboard .settings-info { + padding-top: 1em; + padding-bottom: 1em; +} +.main-right > #maincontent .Dashboard .settings-info p span:nth-child(2) { + max-height: 18.5px; + top: 4px; +} +.main-right > #maincontent .Dashboard .settings-info .label { + font-size: 0.7rem; + padding: 0.2rem 0.6rem; +} +header { + position: relative; + width: 100%; + padding: 0; + color: var(--header-color); +} +header.bg-primary { + background-color: var(--primary) !important; +} +header::after { + content: ""; + position: absolute; + width: 100%; + height: 2rem; + background-color: var(--primary) !important; +} +header .fill { + padding: 0.8rem 0; + display: flex; + border-bottom: 0px solid rgba(255, 255, 255, 0.08) !important; +} +header .fill .container { + width: 100%; + height: 2rem; + padding: 0 1.25rem; + display: flex; + align-items: center; +} +header .fill .container .flex1 { + flex: 1; +} +header .fill .container .flex1 .showSide { + display: none; + font-size: 1.4rem; + color: #fff; +} +header .fill .container .flex1 .showSide:hover { + text-decoration: none; +} +header .fill .container .flex1 .brand { + display: none; + padding-left: 1rem; + font-size: 1.5rem; + font-family: "TypoGraphica"; + color: #fff; + text-decoration: none; + cursor: default; + vertical-align: text-bottom; +} +header .fill .container .pull-right { + margin-top: 0rem; + float: right; + display: flex; +} +header .fill .status span { + display: inline-block; + margin: 0 0.25rem; + padding: 0.3rem 0.8rem; + font-size: 0.875rem; + font-weight: bold; + white-space: nowrap; + text-decoration: none; + text-transform: uppercase; + text-shadow: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); +} +header .fill .status span:last-child { + margin-right: 0; +} +header .fill .status span[data-indicator="poll-status"] { + color: #fff; +} +header .fill .status span[data-style="active"] { + background-color: var(--green); +} +header .fill .status span[data-style="inactive"] { + color: #ffffff !important; + background-color: #32325d; +} +footer { + padding: 1rem; + font-size: 0.875rem; + color: #aaa; + text-align: right; + white-space: nowrap; + overflow: hidden; +} +footer > a { + color: #aaa; + text-decoration: none; +} +.tr.table-titles, +.tr.cbi-section-table-titles { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--background-color); +} +#view { + border-radius: 0.25rem; + display: flex; + flex-direction: column; + gap: 1rem; +} +#view > .spinning { + position: fixed; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + padding: 1rem; + border-radius: 0.5rem; + background: #ffffff; + box-shadow: 0 0 1rem 0 rgba(136, 152, 170, 0.15); +} +#view > div:first-child { + display: flex; + flex-direction: column; + gap: 0.875rem; +} +#xhr_poll_status { + display: flex; + margin-left: 0.5rem; +} +#xhr_poll_status * { + color: #fff; +} +.danger { + background-color: var(--danger) !important; +} +.warning { + background-color: var(--warning) !important; +} +.success { + background-color: var(--success) !important; +} +.notice { + background-color: var(--info) !important; + color: #fff; +} +.error { + color: var(--danger); +} +.alert, +.alert-message { + position: fixed; + width: 20rem; + z-index: 9000; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-weight: bold; + padding: 1rem 1.25rem; + border: 0; + border-radius: 0.25rem !important; + background-color: #fff; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + text-shadow: none; +} +.alert.error, +.alert-message.error { + background-color: var(--warning); +} +.alert h4, +.alert-message h4 { + padding: 0.25rem 0rem; + border-radius: 0.25rem; +} +.alert .btn, +.alert-message .btn { + height: auto; +} +.modal.alert-message { + position: static; + width: 90%; + min-width: 270px; + max-width: 600px; + left: auto; + top: auto; + transform: none; + margin: 5em auto; +} +.alert-message > h4 { + font-size: 110%; + font-weight: bold; +} +.alert-message > * { + margin: 0.5rem 0; +} +.alert-message .btn { + padding: 0.3rem 0.6rem; +} +.container .alert, +.container .alert-message { + margin-left: 0; + margin-right: 0; + margin-top: 0rem; + position: relative; + top: 0; + transform: translate(-50%, 0); + width: 100%; +} +#mwan3-service-status > .alert-message { + left: 0.5rem; + transform: none; +} +.lg { + margin: 0; + padding: 0 !important; +} +.cbi-section, +.cbi-section-error, +#iptables, +.Firewall form, +#cbi-network > .cbi-section-node, +#cbi-wireless > .cbi-section-node, +#cbi-wireless > #wifi_assoclist_table, +[data-tab-title], +[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), +[data-page="admin-system-opkg"] #maincontent > .container { + font-family: inherit; + font-weight: normal; + font-style: normal; + line-height: normal; + min-width: inherit; + padding: 0; + border: 0; + border-radius: 0.25rem; + background-color: #fff; + box-shadow: 0 0 1rem 0 rgba(136, 152, 170, 0.15); +} +.cbi-section:last-child, +.cbi-section-error:last-child, +#iptables:last-child, +.Firewall form:last-child, +#cbi-network > .cbi-section-node:last-child, +#cbi-wireless > .cbi-section-node:last-child, +#cbi-wireless > #wifi_assoclist_table:last-child, +[data-tab-title]:last-child, +[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear):last-child, +[data-page="admin-system-opkg"] #maincontent > .container:last-child { + margin: 0; + border: 0; +} +.cbi-modal .cbi-section, +.cbi-section .cbi-section { + padding: 0; + box-shadow: none; +} +.cbi-modal .cbi-tabmenu { + margin-left: 0; +} +.cbi-map { + display: flex; + flex-direction: column; + gap: 1rem; +} +.cbi-map > .cbi-tabmenu + div { + margin-top: -0.4375rem; +} +.cbi-map-descr { + font-size: small; + line-height: 1.5; + padding: 0 1.25rem; +} +.cbi-section > .cbi-section-descr { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} +.cbi-section > .cbi-section-descr:empty { + display: none; +} +.cbi-section-descr:not(:empty) { + font-size: small; + line-height: 1.5; + padding: 0rem 1rem; +} +.cbi-section-descr:empty { + display: none !important; +} +.cbi-map-descr + fieldset { + margin-top: 1rem; +} +.cbi-map-descr > abbr { + cursor: help; + text-decoration: underline; +} +.cbi-section > legend { + display: none !important; +} +fieldset > fieldset, +.cbi-section > .cbi-section { + margin: 0; + padding: 0; + border: 0; + box-shadow: none; +} +.cbi-section > h3:first-child, +.panel-title { + font-size: 1.1rem; + line-height: 1; + display: block; + width: 100%; + margin: 0; + margin-bottom: 0; + padding: 0.8755rem 1.25rem; + color: #32325d; + color: var(--gray-dark); +} +.cbi-section > h3:first-child, +.cbi-section > h4:first-child, +.cbi-section > p:first-child, +[data-tab-title] > h3:first-child, +[data-tab-title] > h4:first-child, +[data-tab-title] > p:first-child { + padding: 1rem 1.25rem; +} +.cbi-section p { + padding: 1rem; +} +.cbi-tblsection { + overflow-x: auto; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +table, +.table { + overflow-y: hidden; + width: 100%; + font-size: 0.75rem; +} +.table .table-titles th { + background-color: var(--lighter); +} +table > tbody > tr > td, +table > tbody > tr > th, +table > tfoot > tr > td, +table > tfoot > tr > th, +table > thead > tr > td, +table > thead > tr > th, +.table > .tbody > .tr > .td, +.table > .tbody > .tr > .th, +.table > .tfoot > .tr > .td, +.table > .tfoot > .tr > .th, +.table > .thead > .tr > .td, +.table > .thead > .tr > .th, +.table > .tr > .td.cbi-value-field, +.table > .tr > .th.cbi-section-table-cell { + padding: 0.5rem; +} +.container > .cbi-section:first-of-type > .table[width="100%"] > .tr > .td { + padding: 0.6rem; +} +.cbi-section-table-cell { + line-height: 1.1; + align-self: flex-end; + flex: 1 1 auto; +} +tr > td, +tr > th, +.tr > .td, +.tr > .th, +.cbi-section-table-row::before, +#cbi-wireless > #wifi_assoclist_table > .tr:nth-child(2) { + border-top: thin solid #ddd; + padding: 1.1em 1.25rem; +} +#cbi-wireless .td, +.table[width="100%"] > .tr:first-child > .td, +[data-page="admin-network-diagnostics"] .tr > .td, +.tr.table-titles > .th, +.tr.cbi-section-table-titles > .th { + border-top: 0 !important; + background-color: #f6f9fc; + padding: 1.1em 1.25rem; + line-height: 1.3rem; +} +#cbi-network .tr:first-child > .td { + border-top: 0; +} +.table[width="100%"] > .tr:first-child > .td { + margin: auto 0; +} +.cbi-section-table-row { + margin-bottom: 1rem; + text-align: center !important; + background: #f4f4f4; +} +.cbi-section-table-row:last-child { + margin-bottom: 0; +} +.cbi-section-table-row > .cbi-value-field .cbi-dropdown, +.cbi-section-table-row > .cbi-value-field .cbi-input-select, +.cbi-section-table-row > .cbi-value-field .cbi-input-text, +.cbi-section-table-row > .cbi-value-field .cbi-input-password { + width: 100%; +} +.cbi-section-table-row > .cbi-value-field .cbi-input-text, +.cbi-section-table-row > .cbi-value-field .cbi-input-password { + min-width: 80px; +} +.cbi-section-table-row > .cbi-value-field [data-dynlist] > input, +.cbi-section-table-row > .cbi-value-field input.cbi-input-password { + width: calc(100% - 1.5rem); +} +.cbi-section-table-row .td { + text-align: center !important; +} +.cbi-section-table-row .td .cbi-checkbox input[type="checkbox"] { + margin: 0; +} +.control-group { + display: inline-flex; + width: 100%; + flex-wrap: wrap; + gap: 1rem; +} +.control-group input { + border-right-width: 0; + margin-right: 0; +} +.control-group input + button { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + margin-left: 0; + border-left-width: 0; +} +.control-group:has(> input:first-child + .cbi-button) { + gap: 0 !important; +} +.control-group:has(> input:first-child + .cbi-button) input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + width: 15.5rem; + min-width: 15.5rem; +} +.control-group > * { + vertical-align: middle; +} +div > table > tbody > tr:nth-of-type(2n), +div > .table > .tr:nth-of-type(2n) { + background-color: #f9f9f9; +} +/* fix multiple table */ +table table, +.table .table, +.cbi-value-field table, +.cbi-value-field .table, +td > table > tbody > tr > td, +.td > .table > .tbody > .tr > .td, +.cbi-value-field > table > tbody > tr > td, +.cbi-value-field > .table > .tbody > .tr > .td { + border: 0; +} +/* button style */ +.btn, +.cbi-button, +.item::after { + font-size: 0.875rem; + display: inline-block; + width: auto !important; + padding: 0.5rem 0.75rem; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all 0.2s ease-in-out; + text-align: center; + vertical-align: middle; + white-space: nowrap; + text-decoration: none; + border: 0; + border-radius: 0.25rem; + background-color: #f0f0f0; + background-image: none; + appearance: none; + -ms-touch-action: manipulation; + touch-action: manipulation; +} +.btn:not(button) ul:not(.dropdown) li { + padding: 0; +} +.cbi-button-up, +.cbi-button-down { + display: inline-block; + min-width: 0; + padding: 0.2rem 1rem; + font-size: 0; + color: transparent !important; + background: url(../icon/arrow.svg) no-repeat center; + background-size: 12px 20px; +} +.cbi-button-up { + transform: scaleY(-1); +} +.cbi-button:not(select) { + appearance: none !important; +} +.btn:hover, +.btn:focus, +.btn:active, +.cbi-button:hover, +.cbi-button:focus, +.cbi-button:active, +.item:hover::after, +.item:focus::after, +.item:active::after, +.cbi-page-actions .cbi-button-apply + .cbi-button-save:hover, +.cbi-page-actions .cbi-button-apply + .cbi-button-save:focus, +.cbi-page-actions .cbi-button-apply + .cbi-button-save:active { + text-decoration: none; + outline: 0; +} +.btn:hover, +.btn:focus, +.cbi-button:hover, +.cbi-button:focus, +.item:hover::after, +.item:focus::after { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.btn:active, +.cbi-button:active, +.item:active::after { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.cbi-button-up:hover, +.cbi-button-up:focus { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.cbi-button-up:active { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.btn:disabled, +.cbi-button:disabled { + cursor: not-allowed; + pointer-events: none; + opacity: 0.5; + box-shadow: none; +} +/* gray */ +.alert-message [class="btn"], +.modal div[class="btn"], +.cbi-button-find, +.cbi-button-link, +.cbi-button-up, +.cbi-button-down, +.cbi-button-neutral, +.cbi-button[name="zero"], +.cbi-button[name="restart"], +.cbi-button[onclick="hide_empty(this)"] { + color: #fff; + border: thin solid #8898aa; + background-color: #8898aa; +} +/* dark blue */ +.btn.primary, +.cbi-page-actions .cbi-button-save, +.cbi-page-actions .cbi-button-apply + .cbi-button-save, +.cbi-button-add, +.cbi-button-save, +.cbi-button-positive, +.cbi-button-link, +.cbi-button[value="Enable"], +.cbi-button[value="Scan"], +.cbi-button[value^="Back"], +.cbi-button-neutral[onclick="handleConfig(event)"] { + font-weight: normal; + color: #fff !important; + border: thin solid #5e72e4; + border: thin solid var(--primary); + background-color: #5e72e4; + background-color: var(--primary); +} +/* light blue */ +.cbi-page-actions .cbi-button-apply, +.cbi-section-actions .cbi-button-edit, +.cbi-button-edit, +.cbi-button-apply, +.cbi-button-reload, +.cbi-button-action, +.cbi-button[value="Submit"], +.cbi-button[value="Upload"], +.cbi-button[value$="Apply"], +.cbi-button[onclick="addKey(event)"] { + font-weight: normal; + color: #fff !important; + border: thin solid #5e72e4; + border: thin solid var(--primary); + background-color: #5e72e4; + background-color: var(--primary); +} +/* red */ +.btn.danger, +.cbi-section-remove > .cbi-button, +.cbi-button-remove, +.cbi-button-reset, +.cbi-button-negative, +.cbi-button[value="Stop"], +.cbi-button[value="Kill"], +.cbi-button[onclick="reboot(this)"], +.cbi-button-neutral[value="Restart"] { + font-weight: normal; + color: #fff; + border: thin solid #f5365c; + border: thin solid var(--red); + background-color: #f5365c; + background-color: var(--red); +} +/* yellow */ +.btn[value="Dismiss"], +.cbi-button[value="Terminate"], +.cbi-button[value="Reset"], +.cbi-button[value="Disabled"], +.cbi-button[onclick^="iface_reconnect"], +.cbi-button[onclick="handleReset(event)"], +.cbi-button-neutral[value="Disable"] { + font-weight: normal; + color: var(--white); + border: thin solid #eea236; + background-color: #f0ad4e; +} +/* green */ +.cbi-button-success, +.cbi-button-download { + font-weight: normal; + color: var(--white); + border: thin solid #4cae4c; + background-color: #5cb85c; +} +.cbi-page-actions .cbi-button-link:first-child { + float: left; +} +.cbi-page-actions .cbi-dropdown .open { + color: var(--white); +} +.cbi-page-actions .cbi-dropdown .more { + display: block; + width: 1px; + height: 100%; + overflow: hidden; + text-indent: 5000px; + background-color: var(--white); +} +.a-to-btn { + text-decoration: none; +} +.cbi-value-field .cbi-button-add { + font-weight: bold; + padding: 1px 6px; + display: inline-block; + align-items: center; +} +.tabs { + padding: 0 1rem; + background-color: #FFFFFF; + border-radius: 0.25rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); + white-space: nowrap; + overflow-x: auto; +} +.tabs::-webkit-scrollbar { + width: 1px; + height: 5px; +} +.tabs::-webkit-scrollbar-thumb { + background-color: #f6f9fc; +} +.tabs::-webkit-scrollbar-track { + background-color: #fff; +} +.tabs li[class~="active"], +.tabs li:hover { + cursor: pointer; + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + color: #5e72e4; + color: var(--primary); + background-color: #e4e9ee; + margin-bottom: 0; + border-radius: 0; +} +.tabs li[class~="active"] a, +.tabs li:hover a { + color: #5e72e4; + color: var(--primary); +} +.tabs li { + font-size: 0.875rem; + display: inline-block; + padding: 0.875rem 0rem; + border-bottom: 0.18751rem solid rgba(0, 0, 0, 0); + margin: 0; + transition: all 0.2s; +} +.tabs li a { + text-decoration: none; + color: #404040; + padding: 0.5rem 0.8rem; +} +.tabs li:hover { + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); +} +.cbi-tabmenu { + color: white; + padding: 0.5rem 1rem 0 1rem; + white-space: nowrap; + overflow-x: auto; +} +.cbi-tabmenu::-webkit-scrollbar { + width: 1px; + height: 5px; +} +.cbi-tabmenu::-webkit-scrollbar-thumb { + background-color: #f6f9fc; +} +.cbi-tabmenu::-webkit-scrollbar-track { + background-color: #fff; +} +.cbi-tabmenu li { + background: #dce3e9; + display: inline-block; + font-size: 0.875rem; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + padding: 0.5rem 0rem; + border-bottom: 0.18751rem solid rgba(0, 0, 0, 0); + margin: 0 0.2rem; + position: relative; +} +.cbi-tabmenu li a { + text-decoration: none; + color: #404040; + padding: 0.5rem 0.8rem; +} +.cbi-tabmenu li:hover { + cursor: pointer; + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + color: #5e72e4; + color: var(--primary); + background-color: #e4e9ee; + margin-bottom: 0; +} +.cbi-tabmenu li:hover a { + color: #525f7f; +} +.cbi-tabmenu li[class~="cbi-tab"] { + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + color: #5e72e4; + color: var(--primary); + background-color: #e4e9ee; + margin-bottom: 0; +} +.cbi-tabmenu li[class~="cbi-tab"] a { + color: #5e72e4; + color: var(--primary); +} +.cbi-tab-descr { + padding: 0 1.25rem; +} +.cbi-section-node { + display: flex; + flex-direction: column; + gap: 0.875rem; +} +.cbi-section-node > [id^="cbi-config-"] { + display: flex; + flex-direction: column; + gap: 0.875rem; +} +.cbi-section-node > [id^="cbi-config-"]:first-child { + padding-top: 1rem; +} +.cbi-section-node > [id^="cbi-config-"]:last-child { + padding-bottom: 1rem; +} +.cbi-section .cbi-section-remove:nth-of-type(2n), +.container > .cbi-section .cbi-section-node:nth-of-type(2n) { + background-color: #f9f9f9; +} +[data-tab-title] { + overflow: hidden; + height: 0; + opacity: 0; + margin: 0; + padding: 1rem 0; + display: none !important; +} +[data-tab-title] p { + margin-left: 1rem; + margin-bottom: 1rem; +} +[data-tab-active="true"] { + overflow: visible; + height: auto; + opacity: 1; + display: inherit !important; + transition: opacity 0.25s ease-in; + margin: inherit !important; +} +.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3), +.cbi-section[id] .cbi-section-node:nth-of-type(4n+4) { + background-color: #f9f9f9; +} +.cbi-section-node-tabbed { + margin-top: 0; + border: 0 solid #d4d4d4; + border-radius: 0.25rem; +} +.cbi-section-node-tabbed > div { + display: flex; + flex-direction: column; + gap: 0.875rem; +} +.cbi-tabcontainer > .cbi-value:nth-of-type(2n) { + background-color: #f9f9f9; +} +.cbi-value-field { + display: table-cell; +} +.cbi-value-field div + br { + display: none; +} +.cbi-value-description { + line-height: 1.25; + display: table-cell; + padding: 0.5rem 0; + opacity: 0.5; + font-size: 0.75rem; +} +.cbi-value-description abbr { + color: #32325d; + color: var(--gray-dark); +} +.cbi-value-description:empty { + display: none; +} +.cbi-value-title { + display: table-cell; + float: left; + width: 23rem; + padding-right: 2rem; + text-align: right; + word-wrap: break-word; +} +.cbi-value { + display: inline-block; + width: 100%; + padding: 0 1rem; + line-height: 2.4rem; +} +.cbi-value ul { + line-height: 1.25; +} +.cbi-value-field.cbi-dropdown-open .cbi-dropdown { + border-color: var(--primary); + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.cbi-value-field .cbi-dropdown, +.cbi-value-field .cbi-input-select, +.cbi-value input[type="text"], +.cbi-value input[type="password"], +.cbi-value textarea { + min-width: 18rem; +} +#cbi-firewall-zone .cbi-input-select, +#cbi-network-switch_vlan .cbi-input-select { + min-width: 11rem; +} +#cbi-network-switch_vlan .cbi-input-text { + max-width: 3rem; +} +.cbi-input-file { + appearance: auto !important; +} +.cbi-input-file::-webkit-file-upload-button { + display: inline-block !important; +} +.cbi-input-invalid { + color: #f5365c !important; + border-color: #f5365c !important; +} +.cbi-section-error { + font-weight: bold; + line-height: 1.42857143; + margin: 18px; + padding: 6px; + border: thin solid #f5365c; + border-radius: 3px; + background-color: #fce6e6; +} +.cbi-section-error ul { + margin: 0 0 0 20px; +} +.cbi-section-error ul li { + font-weight: bold; + color: #f5365c; +} +.td[data-title]::before { + font-weight: bold; + display: none; + padding: 0.25rem 0; + content: attr(data-title) ":\20"; + text-align: left; + white-space: nowrap; +} +.tr.placeholder .td[data-title]::before { + display: none; +} +.tr[data-title]::before, +.tr.cbi-section-table-titles.named::before { + font-weight: bold; + display: table-cell; + align-self: center; + flex: 1 1 5%; + padding: 0.25rem; + content: attr(data-title) "\20"; + text-align: center; + vertical-align: middle; + white-space: normal; + word-wrap: break-word; + background-color: #f6f9fc; +} +.cbi-rowstyle-1 { + background-color: #f9f9f9; +} +.cbi-rowstyle-2 { + background-color: #eee; +} +.cbi-rowstyle-2 .cbi-button-up, +.cbi-rowstyle-2 .cbi-button-down, +body:not(.Interfaces) .cbi-rowstyle-2:first-child { + background-color: #fff !important; +} +.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell { + width: auto !important; +} +.td.cbi-section-actions { + text-align: right !important; + vertical-align: middle; +} +.td.cbi-section-actions > * { + display: inline-flex; + gap: 1rem; +} +.td.cbi-section-actions > * > *, +.td.cbi-section-actions > * > form > * { + display: flex; + align-items: center; +} +.td.cbi-section-actions > * > form { + display: inline-flex; + margin: 0; +} +.td .cbi-checkbox { + justify-content: center; +} +.cbi-checkbox { + display: flex; + align-items: center; + height: 2.5rem; +} +/* lists */ +.cbi-dynlist { + line-height: 1.3; + flex-direction: column; + min-height: 30px; + cursor: text; + gap: 0.875rem; +} +.cbi-dynlist > .item { + display: inline-flex; + flex-wrap: nowrap; + position: relative; + max-width: 25rem; + pointer-events: none; + color: #8898aa; + outline: 0; +} +.cbi-dynlist > .item::after { + content: "\00D7"; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem !important; + height: 2.5rem; + margin: 0; + font-weight: normal; + font-size: 1rem; + line-height: 1.5rem; + color: #fff; + border: 1px solid #f5365c; + border-radius: 0 0.25rem 0.25rem 0; + outline: 0; + background-color: var(--red); + background-image: none; + box-shadow: none; + box-sizing: border-box; +} +.cbi-dynlist > .item > span { + display: block; + padding: 0.5rem 0.75rem; + min-width: 15.5rem; + width: 15.5rem; + height: 2.5rem; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; + white-space: nowrap; + word-break: break-word; + line-height: 1.5em; + color: #8898aa; + border: 1px solid #dee2e6; + border-radius: 0.25rem 0 0 0.25rem; + outline: 0; + background-image: none; + box-shadow: none; + box-sizing: border-box; + transition: box-shadow 0.15s ease; +} +.cbi-dynlist > .add-item { + display: inline-flex; + align-items: center; + width: 100%; + min-width: 15.5rem; + flex-wrap: nowrap; +} +.cbi-dynlist > .add-item input { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + transition: box-shadow 0.15s ease; + white-space: nowrap; + word-break: break-word; + font-size: 0.875rem; + line-height: 1.5rem; + color: #8898aa; + border: 1px solid #dee2e6; + border-radius: 0.25rem 0 0 0.25rem; + border-right-width: 0; + outline: 0; + background-image: none; + box-shadow: none; +} +.cbi-dynlist > .add-item .cbi-button { + display: flex; + width: auto !important; + padding-left: 0.8rem; + padding-right: 0.8rem; + margin-left: 0; + align-items: center; + justify-content: center; + font-size: 0.875rem; + line-height: 1.5rem; + outline: 0; + background-image: none; + background-color: var(--gray); + box-shadow: none; + color: var(--white); + border-color: var(--gray); + border-radius: 0.25rem; + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.cbi-dynlist > .add-item .cbi-button-add { + width: 2.5rem !important; + padding: 0.5rem 0 !important; + font-weight: normal; + font-size: 1.2rem; + color: #fff; + background-color: var(--primary); + border: 1px solid var(--primary); +} +.cbi-dynlist > .add-item:not([ondrop]) > input { + overflow: hidden; + min-width: 15.5rem; + width: 15.5rem; + white-space: nowrap; + text-overflow: ellipsis; +} +.cbi-dynlist > .item.drag-over, +.cbi-dynlist > .add-item:has(.drag-over) { + border-top: 1px solid var(--red); + margin-top: -1px; +} +.cbi-dynlist[name="sshkeys"] > .item { + max-width: none; +} +.cbi-dynlist > .cbi-dynlist > .add-item[ondrop] > input { + min-width: 13rem; +} +.cbi-dynlist, +.cbi-dropdown { + position: relative; + display: inline-flex; + padding-right: 0.25rem; + min-height: 2.1875rem; +} +.cbi-dropdown { + align-items: center; +} +.cbi-dropdown.btn > ul:not(.dropdown), +.cbi-dropdown.cbi-button > ul:not(.dropdown) { + margin: 0 0.75rem; +} +.cbi-dropdown[placeholder*="select"] { + max-width: 25rem; + height: auto; + margin-top: -3px; +} +.cbi-dropdown > ul { + display: flex; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + padding: 0; + list-style: none; + outline: 0; +} +.cbi-dropdown > ul.preview { + display: none; +} +.cbi-dropdown.cbi-button-apply, +.cbi-dropdown.cbi-button-action { + padding: 0; +} +.cbi-button-apply > ul.preview { + display: none; +} +.cbi-button-apply > ul.preview li { + color: #fff; +} +.cbi-button-apply > ul:first-child li { + color: #fff; +} +.cbi-dropdown > .open { + display: flex; + align-items: center; + width: 1rem; + height: 100%; + padding: 0 0.75rem; + cursor: pointer; + user-select: none; + font-size: 0; + color: #8898aa; + background-color: currentColor; + mask-image: var(--dropdown-arrow-icon); + mask-repeat: no-repeat; + mask-size: 100% 1rem; + mask-position: center; + mask-mode: match-source; +} +.cbi-dropdown > .more, +.cbi-dropdown > ul > li[placeholder] { + display: none; +} +.cbi-dropdown > ul > li { + display: none; + overflow: hidden; + align-items: center; + align-self: center; + flex-grow: 1; + flex-shrink: 1; + min-height: 20px; + padding: 0.125rem 0.25em; + white-space: nowrap; + text-overflow: ellipsis; +} +.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-left: thin solid #ccc; +} +.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 { + margin-right: 0.25em; + vertical-align: middle; +} +.cbi-dropdown > ul > li > form > input[type="checkbox"] { + height: auto; + margin: 0; +} +.cbi-dropdown > ul > li input[type="text"] { + height: 2rem; + min-width: 16rem; + padding: 0 0.5rem; +} +.cbi-dropdown[open] > ul.dropdown { + position: absolute; + z-index: 1100; + display: block; + width: auto; + min-width: 100%; + max-width: none; + max-height: 200px !important; + border: 0 solid #918e8c; + background: #ffffff; + box-shadow: 0 0 4px #918e8c; + border-radius: 0.25rem; + color: var(--main-menu-color); + margin-left: 0 !important; + margin-top: 0.25rem; + left: 0; +} +.cbi-dropdown[open] > ul.dropdown li { + color: #000; +} +.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 { + display: flex; + align-items: center; + flex-grow: 1; +} +.cbi-dropdown[empty] > ul > li, +.cbi-dropdown[optional][open] > ul.dropdown > li[placeholder], +.cbi-dropdown[multiple][open] > ul.dropdown > li > form { + display: block; +} +.cbi-dropdown[open] > ul.dropdown > li .hide-open { + display: none; +} +.cbi-dropdown[open] > ul.dropdown > li .hide-close { + display: initial; +} +.cbi-dropdown[open] > ul.dropdown > li { + border-bottom: thin solid #ccc; + padding: 0.5rem 0.75rem; + cursor: pointer; +} +.cbi-dropdown[open] > ul.dropdown > li label { + margin-left: 0.5rem; +} +.cbi-dropdown[open] > ul.dropdown > li[selected] { + background: #e4e9ee; +} +.cbi-dropdown[open] > ul.dropdown > li.focus { + background: #e4e9ee; + outline: none; +} +.cbi-dropdown[open] > ul.dropdown > li:last-child { + margin-bottom: 0; + border-bottom: 0; +} +.cbi-dropdown[open] > ul.dropdown > li[unselectable] { + opacity: 0.7; +} +.cbi-dropdown[open] > ul.dropdown > li > input.create-item-input:first-child:last-child { + width: 100%; +} +.cbi-dropdown[disabled] { + pointer-events: none; + opacity: 0.6; +} +.cbi-dropdown .zonebadge { + width: 100%; +} +.cbi-dropdown[open] .zonebadge { + width: auto; +} +/* progressbar */ +@keyframes progress-stripes { + 0% { + background-position: 1rem 0; + } + 100% { + background-position: 0 0; + } +} +.cbi-progressbar { + position: relative; + display: flex; + width: 100%; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.5rem; + height: 1rem; + overflow: hidden; +} +.cbi-progressbar > div { + display: block; + position: absolute; + height: 100%; + background-color: var(--primary); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent); + background-size: 1rem 1rem; + border-radius: 0.5rem; + transition: width 0.3s; + animation: progress-stripes 0.75s linear infinite; +} +.cbi-progressbar::after { + content: attr(title); + position: absolute; + font-size: 0.75rem; + color: var(--bs-heading-color); + width: 100%; + height: 100%; + text-align: center; + line-height: 1rem; + z-index: 2; +} +#modal_overlay { + position: fixed; + z-index: 900; + top: 0; + right: 10000px; + bottom: 0; + left: -10000px; + overflow-y: scroll; + transition: opacity 0.125s ease-in; + opacity: 0; + background: rgba(0, 0, 0, 0.7); + -webkit-overflow-scrolling: touch; +} +.modal { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 90%; + min-width: 270px; + max-width: 600px; + min-height: 32px; + margin: 5em auto; + padding: 1rem; + border-radius: 0.25rem !important; + background: #fff; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); +} +.modal > * { + line-height: normal; + flex-basis: 100%; + margin-bottom: 0.5em; + max-width: 100%; +} +.modal > pre, +.modal > textarea { + font-size: 1rem; + font-size-adjust: 0.35; + overflow: auto; + padding: 0.5rem; + cursor: auto; + white-space: pre-wrap; + color: var(--dark-primary); + outline: 0; + border-radius: 0.25rem; + border: 1px solid var(--lighter); + transition: all 0.2s ease; +} +.modal > pre:focus, +.modal > textarea:focus { + border: 1px solid var(--primary); + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} +.modal > h4 { + display: block; + flex-grow: 1; + max-width: none; + padding: 1rem; + margin: -1rem -1rem 0 -1rem; + font-size: 1rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); + border-radius: 0.25rem 0 0 0.25rem; +} +.modal h5 { + margin-top: 1rem; + font-weight: 600; +} +.modal label > input[type="checkbox"] { + top: 0; +} +.modal ul li { + list-style-type: square; + color: #808080; +} +.modal p { + word-break: break-word; + margin-top: 1rem; +} +.modal .label { + font-size: 0.6rem; + font-weight: normal; + padding: 0.1rem 0.3rem; + padding-bottom: 0; + cursor: default; + border-radius: 0; +} +.modal .label.warning { + background-color: #f0ad4e !important; +} +.modal .btn { + padding: 0.45rem 0.8rem; +} +.modal.cbi-modal { + max-width: 90%; + max-height: none; +} +.modal .cbi-map { + margin-bottom: 1rem; +} +body.modal-overlay-active { + overflow: hidden; + height: 100vh; +} +body.modal-overlay-active #modal_overlay { + right: 0; + left: 0; + opacity: 1; +} +.spinning { + position: relative; + padding-left: 32px !important; +} +.spinning::before { + position: absolute; + top: 0; + bottom: 0; + left: 0.2em; + width: 32px; + content: ""; + background: url(/luci-static/resources/icons/loading.svg) no-repeat center; + background-size: 16px; +} +/* luci */ +.hidden { + display: none; +} +.left, +.left::before { + text-align: left !important; +} +.right, +.right::before { + text-align: right !important; +} +.button-row { + display: flex; + flex-direction: row; + gap: 1rem; + align-items: center; + justify-content: flex-end; + margin-bottom: 0 !important; +} +.center, +.center::before { + text-align: center !important; +} +.top { + align-self: flex-start !important; + vertical-align: top !important; +} +.bottom { + align-self: flex-end !important; + vertical-align: bottom !important; +} +.inline { + display: inline; +} +.cbi-page-actions { + padding: 1rem; + justify-content: flex-end; + display: flex; + gap: 1rem; +} +.cbi-page-actions > form[method="post"] { + display: inline-block; +} +.th[data-type="button"], +.td[data-type="button"], +.th[data-type="fvalue"], +.td[data-type="fvalue"] { + flex: 1 1 2em; + text-align: center; +} +.ifacebadge { + display: inline-flex; + align-items: center; + gap: 0.2rem; + padding: 0.25rem 0.8rem; + background: #eee; + border-radius: 4px; +} +td > .ifacebadge, +.td > .ifacebadge { + font-size: 0.875rem; + background-color: #f0f0f0; +} +.ifacebadge > em, +.ifacebadge > img { + display: inline-block; + margin: 0 0.75rem; +} +.ifacebadge > img + img { + margin: 0 0.2rem 0 0; +} +.network-status-table { + display: flex; + flex-wrap: wrap; +} +.network-status-table .ifacebox { + flex-grow: 1; + border-radius: 0.25rem; + overflow: hidden; + margin: 1rem; +} +.network-status-table .ifacebox-body { + display: flex; + flex-direction: column; + height: 100%; + gap: 0.5em; +} +.network-status-table .ifacebox-body > span { + flex: 10 10 auto; +} +.network-status-table .ifacebox-body > div { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + height: 100%; +} +.network-status-table .ifacebox-body .ifacebadge { + align-items: center; + flex: 1 1 auto; + min-width: 220px; + padding: 0.5em; + background-color: #fff; +} +.network-status-table .ifacebox-body .ifacebadge > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +/* textarea */ +.cbi-input-textarea { + font-family: inherit; + width: 100%; + font-size: 0.875rem; + min-height: 14rem; + padding: 0.8rem; + color: #8898aa; + border-radius: 0.25rem; + border: 1px solid #dee2e6; + min-width: 16rem; +} +#content_syslog { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); +} +#syslog { + font-size: small; + font-family: 'Red Hat Display'; + line-height: 1.25; + overflow-y: hidden; + width: 100%; + min-height: 15rem; + padding: 1rem; + resize: none; + color: #242424; + border: 0; + border-radius: 0.25rem; + background-color: #ffffff; +} +#syslog:focus { + outline: 0; +} +#cbi-ddns #syslog { + overflow: auto; +} +/* config changes */ +.uci-change-list { + font-family: inherit; + overflow: scroll; + width: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; +} +.uci-change-list ins, +.uci-change-legend-label ins { + display: block; + padding: 2px; + text-decoration: none; + border: thin solid #0f0; + background-color: #cfc; +} +.uci-change-list del, +.uci-change-legend-label del { + font-style: normal; + display: block; + padding: 2px; + text-decoration: none; + border: thin solid #f00; + background-color: #fcc; +} +.uci-change-list var, +.uci-change-legend-label var { + font-style: normal; + display: block; + padding: 2px; + text-decoration: none; + border: thin solid #ccc; + background-color: #eee; +} +.uci-change-list var ins, +.uci-change-list var del { + font-style: normal; + padding: 0; + white-space: pre; + border: 0; +} +.uci-change-legend { + padding: 5px; +} +.uci-change-legend-label { + float: left; + width: 150px; +} +.uci-change-legend-label > ins, +.uci-change-legend-label > del, +.uci-change-legend-label > var { + display: block; + float: left; + width: 10px; + height: 10px; + margin-right: 4px; +} +.uci-change-legend-label var ins, +.uci-change-legend-label var del { + line-height: 0.4; + border: 0; +} +.uci-change-list var, +.uci-change-list del, +.uci-change-list ins { + padding: 0.5rem; +} +.uci-dialog .cbi-section { + padding: 0.5rem; +} +.uci-dialog .cbi-section .uci-change-legend { + line-height: 15px; + padding: 10px 20px 0 20px; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label { + padding: 0; + margin: 0; + position: relative; + float: none; + display: inline-block; + width: 25%; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > ins, +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > del { + width: 14px; + height: 14px; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var { + position: relative; + width: 14px; + height: 14px; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var ins, +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var del { + position: absolute; + left: 2px; + top: 2px; + right: 2px; + bottom: 2px; +} +.uci-dialog .cbi-section .uci-change-list { + overflow: auto; +} +.uci-dialog .cbi-section .uci-change-list + .right .btn { + color: #333; +} +.uci-dialog .cbi-section .uci-change-list + .right .cbi-dropdown ul:not(.dropdown) li { + color: #fff; +} +.uci-dialog .cbi-section .uci-change-list + .right .cbi-button { + padding: 0.45rem 0.8rem; +} +/* other fix */ +#iwsvg, +#iwsvg2, +#bwsvg { + border: thin solid #d4d4d4 !important; +} +#iwsvg { + border-top: 0 !important; +} +.ifacebox { + line-height: 1.25; + display: inline-flex; + overflow: hidden; + flex-direction: column; + border-radius: 4px; + min-width: 100px; + background-color: #f9f9f9; +} +.ifacebox-head { + padding: 0.25em; + background: #eee; +} +.ifacebox-head.active { + background: #5e72e4; + background: var(--primary); +} +.ifacebox-head.active * { + color: #fff; + color: var(--white); +} +.ifacebox-body { + padding: 0.875rem 1rem; + line-height: 1.6em; +} +.cbi-image-button { + margin-left: 0.5rem; +} +.zonebadge { + display: inline-block; + padding: 0.2rem 0.5rem; + border-radius: 4px; +} +.zonebadge .ifacebadge { + margin: 0.1rem 0.2rem; + padding: 0.2rem 0.3rem; + border: thin solid #6c6c6c; +} +.zonebadge > input[type="text"] { + min-width: 10rem; + margin-top: 0.3rem; + padding: 0.16rem 1rem; +} +.zonebadge > em, +.zonebadge > strong { + display: inline-block; + margin: 0 0.2rem; +} +.cbi-value-field .cbi-input-checkbox, +.cbi-value-field .cbi-input-radio { + margin-top: 0.1rem; +} +.cbi-value-field > ul > li { + display: flex; +} +.cbi-value-field > ul > li > label { + margin-top: 0.5rem; +} +.cbi-value-field > ul > li .ifacebadge { + margin-top: -0.5rem; + margin-left: 0.4rem; + background-color: #eee; +} +.cbi-section-table-row > .cbi-value-field .cbi-dropdown { + min-width: 3rem; +} +.cbi-section-create { + display: inline-flex; + align-items: center; + padding: 0.875rem 1rem; +} +.cbi-section-create > div:first-child > input { + width: 100%; + border-right: none; + border-bottom-right-radius: 0; + border-top-right-radius: 0; +} +.cbi-section-create > div:first-child + button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.cbi-section-remove { + padding: 0.5rem 1rem; +} +div.cbi-value var, +td.cbi-value-field var, +.td.cbi-value-field var { + font-style: italic; + color: #0069d6; +} +.cbi-optionals { + padding: 1rem 1rem 0 1rem; + border-top: thin solid #ccc; +} +.cbi-dropdown-container { + position: relative; +} +.cbi-tooltip-container, +span[data-tooltip], +span[data-tooltip] .label { + cursor: help !important; +} +.cbi-tooltip { + position: absolute; + z-index: 1000; + left: -10000px; + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); + border-radius: 0.25rem; + background: #fff; + white-space: pre; + padding: 0.5rem; + opacity: 0; + transition: opacity 0.25s ease-in; + transform: translate(-50%, 10%); +} +.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + left: auto; + transition: opacity 0.25s ease-in; + opacity: 1; +} +.zonebadge .cbi-tooltip { + margin: -1.5rem 0 0 -0.5rem; + padding: 0.25rem; + background: inherit; +} +.zonebadge-empty { + color: #404040; + background: repeating-linear-gradient(45deg, rgba(204, 204, 204, 0.5), rgba(204, 204, 204, 0.5) 5px, rgba(255, 255, 255, 0.5) 5px, rgba(255, 255, 255, 0.5) 10px); +} +.zone-forwards { + display: flex; + min-width: 10rem; +} +.zone-forwards > * { + flex: 1 1 45%; +} +.zone-forwards > span { + flex-basis: 10%; + padding: 0 0.25rem; + text-align: center; +} +.zone-forwards .zone-src, +.zone-forwards .zone-dest { + display: flex; + flex-direction: column; +} +.label { + font-size: 0.875rem; + font-weight: bold; + padding: 0.3rem 0.8rem; + white-space: nowrap; + text-decoration: none; + text-transform: uppercase; + color: #fff !important; + border-radius: 3px; + background-color: #bfbfbf; + text-shadow: none; +} +label > input[type="checkbox"], +label > input[type="radio"] { + position: relative; + top: 0.4rem; + right: 0.2rem; + margin: 0; + vertical-align: bottom; +} +label[data-index][data-depends] { + padding-right: 2em; +} +.showSide { + display: none; +} +.darkMask { + position: fixed; + z-index: 99; + display: none; + width: 100%; + height: 100%; + content: ""; + top: 0; + background-color: rgba(0, 0, 0, 0.56); + transition: all 0.2s; +} +.darkMask.active { + display: block; +} +/* diagnostics */ +#diag-rc-output > pre, +#command-rc-output > pre, +[data-page="admin-services-wol"] .notice code { + font-size: 1.2rem; + font-size-adjust: 0.35; + line-height: normal; + display: block; + overflow-y: hidden; + width: 100%; + padding: 8.5px; + white-space: pre; + color: #eee; + background-color: #101010; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); +} +input[name="ping"], +input[name="traceroute"], +input[name="nslookup"] { + width: 80%; +} +.controls { + gap: 0.5rem; + margin: 0 1.25rem !important; +} +.controls > div { + gap: 0.5rem; +} +.controls > * > .btn:not([aria-label$="page"]) { + flex-grow: initial !important; + margin-top: 0.25rem; +} +.controls > #pager > .btn[aria-label$="page"] { + font-size: 1.4rem; + font-weight: bold; +} +.controls > * > label { + margin-bottom: 0.2rem; +} +.td.version, +.td.size { + white-space: normal !important; + word-break: break-word; +} +.cbi-tabmenu + .cbi-section { + margin-top: 0; +} +.cbi-tab-disabled[data-errors]::after { + position: absolute; + top: -0.25rem; + right: -0.25rem; + content: attr(data-errors); + background-color: var(--red); + color: #fff; + width: 0.875rem; + height: 0.875rem; + border-radius: 0.875rem; + text-align: center; + display: inline-flex; + flex-direction: column; + justify-content: center; + font-size: 0.75em; +} +/* custom commands */ +.commands { + display: grid !important; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 0.75rem; +} +.commandbox { + padding: 0.5rem 1rem; + gap: 0.5rem; + border-bottom: thin solid #ccc; + border-radius: 0.25rem; + background: #eee; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05); +} +.commandbox h3 { + line-height: normal !important; + overflow: hidden; + margin: 6px 0 !important; + white-space: nowrap; + text-overflow: ellipsis; +} +.commandbox div { + left: auto !important; +} +.commandbox code { + overflow: hidden; + max-width: fit-content; + padding: 2px 3px; + white-space: nowrap; + text-overflow: ellipsis; +} +.commandbox code:hover { + overflow-y: auto; + max-height: 50px; + white-space: normal; +} +.commandbox p:first-of-type { + margin-top: -6px; +} +.commandbox p:nth-of-type(2) { + margin-top: 2px; +} +#command-rc-output .alert-message { + line-height: 1.42857143; + position: absolute; + top: 40px; + right: 32px; + max-width: 40%; + margin: 0; + animation: anim-fade-in 1.5s forwards; + word-break: break-word; + opacity: 0; +} +@keyframes anim-fade-in { + 100% { + opacity: 1; + } +} +/* other fix */ +.fb-container .cbi-button { + height: auto !important; +} +#cbi-usb_printer-printer em { + display: block; + padding: 1rem; + text-align: center; +} +pre.command-output { + padding: 1.5rem; +} +/* page fix */ +[data-page="admin-system-autoreboot"] #cbi-autoreboot { + margin-top: 0; +} +[data-page="admin-system-system"] .control-group { + gap: 0.875rem; +} +[data-page="admin-system-system"] .cbi-dynlist { + margin: 0.25rem 0; +} +[data-page="admin-system-admin"] .cbi-map h2, +[data-page="admin-system-admin-password"] .cbi-map h2, +[data-page="admin-system-admin"] .cbi-map .cbi-map-descr, +[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr { + margin-left: 0; + color: var(--gray-dark); +} +[data-page="admin-system-admin"] .cbi-section-node, +[data-page="admin-system-admin-password"] .cbi-section-node { + padding: 1rem 0; +} +[data-page="admin-system-admin-sshkeys"] .cbi-dynlist { + margin-left: 1rem; +} +[data-page="admin-system-startup"] #view > div:first-child div[data-initialized="true"] { + margin-top: -0.4375rem; +} +[data-page="admin-system-startup"] [data-tab-title] p { + margin-left: 0; + margin-bottom: 0; + display: flex; + align-items: center; + justify-content: center; +} +[data-page="admin-system-startup"] textarea { + line-height: 1.25; + overflow-y: auto; + width: 100%; + min-height: 15rem; + margin: 0 1rem; + padding: 1rem; + resize: none; + color: #8898aa; + border-radius: 0.25rem; + border: 1px solid #dee2e6; +} +[data-page="admin-system-startup"] textarea:focus-visible { + outline: none; + box-shadow: none; + border: 1px solid var(--primary); +} +[data-page="admin-system-crontab"] #view p textarea { + line-height: 1.25; + overflow-y: hidden; + width: 100%; + min-height: 15rem; + padding: 1rem; + resize: none; + background-color: transparent; + background: var(--white); + outline: none; + color: #8898aa; + border-radius: 0.25rem; + border: 1px solid #dee2e6; + transition: all 0.2s ease; +} +[data-page="admin-system-crontab"] #view p textarea:focus-visible { + outline: none; + box-shadow: none; + border: 1px solid var(--primary); +} +[data-page="admin-system-crontabhelper"] .crontab-row .dropdown-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} +[data-page="admin-system-crontabhelper"] .crontab-row .dropdown-container div { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} +[data-page="admin-system-partexp-global"] .cbi-section { + padding: 1rem 0; +} +[data-page="admin-system-attendedsysupgrade"] #view .cbi-button { + margin-left: 0 !important; + margin-top: 1rem !important; +} +[data-page="admin-system-attendedsysupgrade-configuration"] .cbi-map .cbi-map-descr { + padding-bottom: 0; +} +[data-page="admin-system-flash"] .cbi-value { + padding: 0 1rem; +} +[data-page="admin-system-flash"] .cbi-section .cbi-section { + margin-top: 0; +} +[data-page="admin-system-flash"] .cbi-map-tabbed { + border-radius: 0.25rem; +} +[data-page="admin-system-flash"] .cbi-section-node { + padding-top: 0; + padding-bottom: 0.5rem; +} +[data-page="admin-system-flash"] legend { + font-size: 1.2rem; + width: 100%; + display: block; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + line-height: 1.5; + margin-bottom: 0; + letter-spacing: 0.1rem; + color: #32325d; + font-weight: bold; + padding: 1rem 0 1rem 1rem; +} +[data-page="admin-system-flash"] .cbi-section-descr { + font-weight: 600; + padding: 1rem 0 1rem 1rem; + color: #525f7f; +} +[data-page="admin-system-flash"] .cbi-page-actions { + padding: 0rem 1rem 1rem 0rem; +} +[data-page="admin-system-flash"] .modal label > input[type="checkbox"] { + top: -0.25rem; +} +[data-page="admin-system-flash"] .modal .btn { + white-space: normal !important; +} +[data-page="admin-system-flash"] .modal label > input[type="checkbox"] { + vertical-align: text-top; + top: auto; +} +[data-page="admin-system-filetransfer"] #cbi-upload { + margin-top: 0; +} +[data-page="admin-system-filetransfer"] .cbi-section-table { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); +} +[data-page="admin-system-fileassistant"] #upload-toggle { + display: none !important; +} +[data-page="admin-system-fileassistant"] .fb-container .panel-title { + padding: 0.5rem 0.75rem !important; +} +[data-page="admin-system-fileassistant"] .cbi-section.fb-container { + padding: 0.5rem; +} +[data-page="admin-system-fileassistant"] .cbi-section.fb-container label.cbi-value-title { + line-height: 1.5rem; + padding: 0.5rem 0.75rem; + width: 60px; + text-align: left; +} +[data-page="admin-system-fileassistant"] .cbi-section.fb-container div.cbi-value-field { + width: 100%; + display: block; + padding-left: 60px; +} +[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content table.cbi-section-table thead td.cbi-section-table-cell, +[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content table.cbi-section-table tbody td.cbi-section-table-cell { + width: 232px; +} +[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody tr:nth-child(1) td.parent-icon strong { + margin-left: 0 !important; +} +[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody td.cbi-value-field.file-icon strong, +[data-page="admin-system-fileassistant"] .cbi-section.fb-container div#list-content tbody td.cbi-value-field.folder-icon strong { + vertical-align: middle; + margin-left: 5px; +} +[data-page="admin-system-fileassistant"] .fb-container .panel-container { + border-bottom-color: #dee2e6; + display: flex; +} +[data-page="admin-system-fileassistant"] .fb-container .panel-container .panel-title { + flex: 1; +} +[data-page="admin-system-fileassistant"] .fb-container .upload-container { + display: flex !important; +} +[data-page="admin-system-fileassistant"] .fb-container .upload-container .upload-file { + flex: 1; + margin-right: 0 !important; +} +[data-page="admin-system-opkg"] h2 { + margin-left: 0; + color: var(--gray-dark); +} +[data-page="admin-system-opkg"] input[name="filter_i18n"] { + top: 2px; + vertical-align: text-top; +} +[data-page="admin-system-opkg"] div.btn { + line-height: 3; + display: inline; + padding: 0.3rem 0.6rem; +} +[data-page="admin-system-opkg"] #maincontent > .container { + margin: 0 1.25rem 1rem 1.25rem; + margin-bottom: 1rem; +} +[data-page="admin-system-package-manager"] .controls div:nth-child(5) > label { + margin-bottom: 5px; +} +[data-page="admin-system-package-manager"] .controls div:nth-child(5) > div label { + margin-top: 1rem; +} +[data-page="admin-system-package-manager"] .controls div:nth-child(5) > div label input { + top: -2px; + right: 0; + vertical-align: middle; +} +[data-page="admin-system-reboot"] p { + padding-left: 1.5rem; +} +[data-page="admin-system-reboot"] p > span { + position: relative; + top: 0.1rem; + left: 1rem; +} +[data-page="admin-system-reboot"] .cbi-button { + background: #fb6340 !important; + border-color: #fb6340 !important; + margin-left: 0 !important; +} +[data-page="admin-system-reboot"] .cbi-button.cbi-button-action.important { + align-self: flex-end; +} +/* applyreboot fix */ +#applyreboot-container { + margin: 2rem; +} +#applyreboot-section { + line-height: 300%; + margin: 2rem; +} +[data-page="admin-system-poweroff"] .container h2 + br + p { + margin-bottom: 1rem; + padding-left: 1.5rem; +} +[data-page="admin-system-poweroffdevice"] .container h2 { + margin: 0; +} +[data-page="admin-system-poweroffdevice"] .container h2 + p { + margin-bottom: 1rem; + padding-left: 1.5rem; +} +[data-page="admin-system-poweroffdevice"] .container button + div { + display: none; +} +[data-page="admin-system-poweroffdevice"] .btn.cbi-button.cbi-button-negative { + align-self: flex-end; +} +[data-page="admin-network-network"] .cbi-value-field .cbi-dynlist { + padding: 0 !important; +} +[data-page="admin-network-network"] .td > .ifacebadge > .cbi-tooltip-container { + display: flex; +} +[data-page="admin-network-network"] .td > .ifacebadge > .cbi-tooltip-container img { + vertical-align: middle; +} +[data-page="admin-network-network"] div[data-name="_gen_server_keypair"] .cbi-value-title, +[data-page="admin-network-network"] div[data-name="_gen_peer_keypair"] .cbi-value-title, +[data-page="admin-network-network"] div[data-name="_gen_psk"] .cbi-value-title { + height: 2.4rem; +} +[data-page="admin-network-network"] #modal_overlay > .modal.cbi-modal > div > p > textarea { + height: 20em !important; + border: 1px solid #dee2e6 !important; + border-radius: 4px; +} +[data-page="admin-network-dhcp"] .cbi-value { + padding: 0; +} +[data-page="admin-network-dhcp"] [data-tab-active="true"] { + padding: 1rem 1rem !important; +} +[data-page="admin-network-diagnostics"] .table { + box-shadow: none; +} +[data-page="admin-network-diagnostics"] .cbi-section { + padding: 1rem; + font-family: monospace; + background: #fff !important; +} +[data-page="admin-network-diagnostics"] textarea { + background: transparent; + border-radius: 0.25rem; + color: #8898aa; + border: 1px solid #dee2e6; + padding: 0.5rem; +} +[data-page="admin-network-diagnostics"] .tr { + display: flex; + flex-direction: row; +} +[data-page="admin-network-diagnostics"] .tr .td { + background-color: #fff !important; + border-bottom: 1px solid #dee2e6 !important; + display: flex; + flex-direction: row; + align-items: center; + flex: 1; +} +[data-page="admin-network-diagnostics"] .tr .td input { + margin: 0 !important; + border-right: none !important; + border-bottom-right-radius: 0 !important; + border-top-right-radius: 0 !important; +} +[data-page="admin-network-diagnostics"] .tr .td .cbi-dropdown, +[data-page="admin-network-diagnostics"] .tr .td .cbi-button { + height: 2.5rem; + border-left: none !important; + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; +} +[data-page="admin-network-diagnostics"] .tr .td .cbi-dropdown .more, +[data-page="admin-network-diagnostics"] .tr .td .cbi-button .more { + display: block; + width: 1px; + height: 100%; + overflow: hidden; + text-indent: 5000px; + background-color: var(--white); +} +[data-page="admin-network-diagnostics"] .cbi-dropdown .open { + color: var(--white); +} +[data-page="admin-network-firewall-custom"] #view p, +[data-page="admin-status-routes"] #view p { + padding: 0 1.25rem; +} +[data-page="admin-network-firewall-custom"] #view p textarea, +[data-page="admin-status-routes"] #view p textarea { + padding: 1rem; + border-radius: 0.25rem; +} +[data-page="admin-network-firewall-custom"] #view > h3, +[data-page="admin-status-routes"] #view > h3 { + border-radius: 0.25rem 0.25rem 0 0; +} +[data-page="admin-network-firewall-custom"] #view .cbi-tabmenu + div, +[data-page="admin-status-routes"] #view .cbi-tabmenu + div { + margin-top: -0.5rem; +} +.cbi-section.fade-in .cbi-title { + position: relative; +} +.cbi-section.fade-in .cbi-title > div:last-child { + position: absolute; + right: 1.25rem; + top: 0; +} +.cbi-section.fade-in .cbi-title > div:last-child span { + font-size: 0; + background: none!important; + color: #ced4da !important; +} +.cbi-section.fade-in .cbi-title > div:last-child span:after { + transition: all 0.3s; + font-family: 'noobwrt' !important; + content: '\e90f'; + font-size: 1.1rem; + padding: 0.8755rem 0; + position: absolute; + right: 0; +} +.cbi-section.fade-in .cbi-title > div:last-child span[data-style=inactive]:after { + transform: rotate(90deg); +} +[data-page="admin-status"] #view > div:first-child, +[data-page="admin-status-overview"] #view > div:first-child { + gap: 0 !important; +} +[data-page="admin-status-iptables"] .right { + margin-bottom: 0 !important; +} +[data-page="admin-status-nftables"] .nft-chain-hook { + padding: 0.5rem 1.25rem; +} +[data-page="admin-status-nftables"] .alert-message { + opacity: 1; +} +[data-page="admin-status-nftables"] .alert-message.fade-out { + opacity: 0; +} +[data-page="admin-services-nlbw-display"] .cbi-section[data-tab="export"] { + padding: 1.5rem !important; +} +[data-page="admin-services-nlbw-display"] .cbi-section > .right { + margin: 15px 15px 0px 0px; +} +[data-page="admin-services-ddns"] #syslog { + overflow: auto; + font-size: 12px !important; +} +[data-page="admin-services-ttyd"] .container { + display: flex; + flex-direction: column; +} +[data-page="admin-services-ttyd"] #view { + flex: 1; +} +[data-page="admin-services-ttyd"] #view iframe { + height: 100%; +} +#cbi-dockerd .cbi-section { + box-sizing: border-box; + padding: 16px; +} +#cbi-dockerd .cbi-section.cbi-tblsection { + padding: 0; +} +#cbi-smartdns #service_status { + padding: 16px; +} +#cbi-smartdns .cbi-section-node { + padding: 16px; +} +#cbi-docker .cbi-section .cbi-section-node { + box-sizing: border-box; + padding: 16px; +} +[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] { + white-space: normal; +} +[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] input[id="widget.cbid.npc.config.vkey"][type="password"] { + margin-top: 0; + margin-bottom: 0; +} +[data-page="admin-services-npc"] div[id="cbid.npc.config.vkey"] .cbi-button { + margin: 0; + border: none; +} +[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".server_addr"]:not([id="cbid.npc.config.server_addr"]) + br, +[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".vkey"]:not([id="cbid.npc.config.vkey"]) + br, +[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".protocol"]:not([id="cbid.npc.config.protocol"]) + br, +[data-page="admin-services-npc"] div[id^="cbid.npc."][id$=".dns"]:not([id="cbid.npc.config.dns"]) + br { + display: none; +} +[data-page="admin-services-npc"] div.cbi-value.nowrap { + white-space: normal; +} +[data-page^="admin-services-openclash"] .oc { + --bg-light: #fff; + --bg-gray: #fff; + border-radius: 8px; +} +[data-page^="admin-services-openclash"] .oc .developer-container { + background: transparent; +} +[data-page^="admin-services-openclash"] .oc .announcement-card { + width: auto!important; + margin: 10px!important; +} +[data-page^="admin-services-openclash"] #cbi-openclash > fieldset { + background: none; + padding: 0; +} +[data-page^="admin-services-openclash"] #cbi-openclash > fieldset > table > tbody > tr > td { + border: none; + padding: 0; +} +[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .main-card { + border: none; + box-shadow: none; + padding: 0 10px 10px 10px; + background: transparent; +} +[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .myip-main-card { + margin: 0; + padding: 10px; + border: none; + background: transparent; +} +[data-page^="admin-services-openclash"] #cbi-openclash > fieldset .myip-main-card .myip-section-title { + margin: 0; + border-width: 1px; +} +[data-page^="admin-services-openclash"] .sub-card .card-title { + margin-top: 5px; +} +[data-page^="admin-services-openclash"] .oc .main-cards-container { + margin: 0 !important; + gap: 0 !important; +} +[data-page^="admin-services-openclash"] .oc .config-file-bottom { + margin-bottom: 5px; +} +[data-page^="admin-services-openclash"] .oc .config-file-bottom .card-actions { + margin-bottom: 2px !important; +} +[data-page^="admin-services-openclash"] .oc .card-content { + align-items: center !important; + justify-content: center !important; +} +[data-page^="admin-services-openclash"] .oc .card-controls { + align-self: center; +} +[data-page^="admin-services-openclash"] .oc .core-main-controls { + justify-content: center !important; +} +[data-page^="admin-services-openclash"] .oc .plugin-toggle-container { + margin-left: inherit !important; +} +[data-page^="admin-services-openclash"] .oc .core-status-toggle { + flex: 0 !important; + min-width: auto !important; +} +[data-page^="admin-services-openclash"] .oc .config-upload-content { + background: #fff !important; +} +[data-page^="admin-services-openclash"] .oc .subscription-info-container { + background: transparent !important; +} +[data-page^="admin-services-openclash"] .cbi-tabmenu > li { + border-right: none !important; + margin: 0 0.4rem 0 0 !important; +} +[data-page^="admin-services-openclash"] .cbi-tabmenu > li:last-child { + margin-right: 0 !important; +} +[data-page^="admin-services-openclash"] #tab-content .dom { + padding: 0 1rem 1rem 1rem; +} +[data-page^="admin-services-openclash"] .cbi-input-file { + padding: 0.2813rem; + width: 15rem !important; +} +[data-page^="admin-services-openclash"] [id="container.openclash.config.debug"] fieldset { + border: none !important; + padding: 1rem !important; +} +[data-page^="admin-services-openclash"] #diag-rc-output > pre, +[data-page^="admin-services-openclash"] #dns-rc-output > pre { + font-size: 0.875rem; + color: #8898aa; + border: 1px solid #dee2e6; + background-color: transparent; + border-radius: 0.25rem; + font-family: 'Google Sans' !important; + box-shadow: none; +} +[data-page^="admin-services-openclash"] #debug-rc-output > textarea { + font-family: 'Google Sans' !important; +} +[data-page^="admin-services-openclash"] .CodeMirror { + font-size: inherit; + font-family: 'Google Sans' !important; +} +[data-page^="admin-services-openclash"] .cbi-button-up, +[data-page^="admin-services-openclash"] .cbi-button-down { + padding: 0.8rem 1.5rem; + background-color: #f1f1f1; + font-size: 0; +} +[data-page^="admin-services-openclash"] select#CORE_VERSION, +[data-page^="admin-services-openclash"] select#RELEASE_BRANCH { + width: auto; +} +[data-page="admin-vpn-passwall"] h4 { + background: transparent; +} +/* openvpn bug fix */ +.OpenVPN a { + line-height: initial !important; +} +/* wireless overview */ +#cbi-wireless > #wifi_assoclist_table > .tr { + box-shadow: inset 1px -1px 0 #ddd, inset -1px -1px 0 #ddd; +} +#cbi-wireless > #wifi_assoclist_table > .tr.placeholder > .td { + right: 2rem; + bottom: 2rem; + left: 2rem; + border-top: thin solid #ddd !important; +} +#cbi-wireless > #wifi_assoclist_table > .tr.table-titles { + box-shadow: inset 1px 0 0 #ddd, inset -1px 0 0 #ddd; +} +#cbi-wireless > #wifi_assoclist_table > .tr.table-titles > .th { + border-bottom: thin solid #ddd; + box-shadow: 0 -1px 0 0 #ddd; +} +#wifi_assoclist_table > .tr > .td[data-title="RX Rate / TX Rate"] { + width: 23rem; +} +/* samba */ +#cbi-samba [data-tab="template"] .cbi-value-field { + display: block; +} +#cbi-samba [data-tab="template"] .cbi-value-title { + width: auto; + padding-bottom: 0.6rem; +} +/* fix status */ +.node-status-overview > .main fieldset:nth-child(4) .td:nth-child(2), +.node-status-processes > .main .table .tr .td:nth-child(3) { + white-space: normal; +} +div[style*="display:grid;grid-template-columns:repeat"] { + display: flex !important; + justify-content: space-evenly !important; + flex-wrap: wrap; +} +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox { + text-align: center; + flex-basis: 100px; +} +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body { + font-size: 0.7rem; + padding: 0.875rem; +} +div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body .cbi-tooltip-container { + font-size: inherit !important; +} +/* firewall */ +#iptables { + margin: 0; +} +.Firewall form { + margin: 2rem 2rem 0 0; + padding: 0; + box-shadow: none; +} +#cbi-firewall-redirect table *, +#cbi-network-switch_vlan table *, +#cbi-firewall-zone table * { + font-size: small; +} +#cbi-firewall-redirect table input[type="text"], +#cbi-network-switch_vlan table input[type="text"], +#cbi-firewall-zone table input[type="text"] { + width: 5rem; +} +#cbi-firewall-redirect table select, +#cbi-network-switch_vlan table select, +#cbi-firewall-zone table select { + min-width: 3.5rem; +} +#cbi-network-switch_vlan .th, +#cbi-network-switch_vlan .td { + flex-basis: 12%; +} +#cbi-network-switch_vlan .table { + display: block; +} +#cbi-network-switch_vlan .td { + width: 100%; +} +#cbi-firewall-zone .table { + display: block; +} +#cbi-firewall-zone .td { + width: 100%; +} +[data-page^="admin-system-commands"] .panel-title, +[data-page^="command-cfg"] .mobile-hide, +[data-page^="command-cfg"] .showSide { + display: none; +} +#mwan3-service-status { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} +#mwan3-service-status > .alert-message { + position: static; + transform: none; +} +/* responsive */ +@media screen and (max-width: 1600px) { + header > .fill > .container > #logo { + margin: 0 2.5rem 0 0.5rem; + } + .main-left { + width: 13rem; + } + .btn:not(button), + .label { + padding: 0.5rem 0.75rem; + } + .cbi-value-title { + width: 15rem; + padding-right: 0.6rem; + } + .cbi-value-field .cbi-dropdown, + .cbi-value-field .cbi-input-select, + .cbi-value input[type="text"], + .cbi-value input[type="password"], + .cbi-value textarea { + min-width: 18rem; + } + #cbi-firewall-zone .cbi-input-select { + min-width: 9rem; + } + .cbi-input-textarea { + font-size: small; + } + .node-admin-status > .main fieldset li > a { + padding: 0.3rem 0.6rem; + } +} +@media screen and (max-width: 1366px) { + header > .fill > .container { + cursor: default; + } + .main-left { + width: 13rem; + } + .tabs > li > a, + .cbi-tabmenu > li > a { + padding: 0.2rem 0.8rem; + } + .panel-title { + font-size: 1.1rem; + padding-bottom: 1rem; + } + table { + width: 100% !important; + } + .table .cbi-input-text { + width: 100%; + } + .cbi-value-field .cbi-dropdown, + .cbi-value-field .cbi-input-select, + .cbi-value input[type="text"], + .cbi-value input[type="password"] { + min-width: 16rem; + } + #cbi-firewall-zone .cbi-input-select { + min-width: 5.5rem; + } + .main > .main-left > .nav > li, + .main > .main-left > .nav > li > a, + .main .main-left .nav > li > a:first-child, + .main > .main-left > .nav > .slide > .menu, + .main > .main-left > .nav > li > [data-title="Log_out"] { + font-size: 0.9rem; + } + .main > .main-left > .nav > .slide > .slide-menu > li > a { + font-size: 0.875rem; + } + #modal_overlay { + top: 0rem; + } + [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) { + display: block; + } + [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table), + [data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table), + [data-page="admin-network-hosts"] .table, + [data-page="admin-network-routes"] .table { + overflow-y: visible; + } + .btn:not(button), + .cbi-button { + font-size: 0.875rem; + } +} +@media screen and (max-width: 1152px) { + header > .fill > .container > #logo { + display: none; + } + header > .fill > .container > .brand { + position: relative; + } + html, + .main { + overflow-y: visible; + } + .main > .loading > span { + top: 25%; + } + .main-left { + width: 13rem; + } + body:not(.logged-in) .showSide { + visibility: hidden; + width: 0; + margin: 0; + } + .node-main-login > .main .cbi-value-title { + text-align: left; + } + .cbi-value-title { + width: 12rem; + padding-right: 1rem; + } + .cbi-value-field .cbi-dropdown, + .cbi-value-field .cbi-input-select, + .cbi-value input[type="text"] { + width: 16rem; + min-width: 16rem; + } + .cbi-value input[name^="pw"], + .cbi-value input[data-update="change"]:nth-child(2) { + width: 13rem !important; + min-width: 13rem; + } + #diag-rc-output > pre, + #command-rc-output > pre, + [data-page="admin-services-wol"] .notice code { + font-size: 1rem; + } + .table { + display: block; + } + .Interfaces .table { + overflow-x: hidden; + } + #packages.table { + display: grid; + } + .tr { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + .Overview .table[width="100%"] > .tr { + flex-wrap: nowrap; + } + .tr.placeholder { + border-bottom: thin solid #ddd; + } + .tr.placeholder > .td, + #cbi-firewall .tr > .td, + #cbi-network .tr:nth-child(2) > .td, + .cbi-section #wifi_assoclist_table .tr > .td { + border-top: 0; + } + .th, + .td { + display: inline-block; + align-self: flex-start; + flex: 2 2 10%; + text-overflow: ellipsis; + word-wrap: break-word; + } + .td select, + .td input[type="text"] { + width: 100%; + word-wrap: normal; + } + .td [data-dynlist] > input, + .td input.cbi-input-password { + width: calc(100% - 1.5rem); + } + .td[data-type="button"], + .td[data-type="fvalue"] { + flex: 1 1 12.5%; + text-align: left; + } + .th.cbi-value-field, + .td.cbi-value-field, + .th.cbi-section-table-cell, + .td.cbi-section-table-cell { + flex-basis: auto; + padding-top: 1rem; + } + .cbi-section-table-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + .td.cbi-value-field, + .cbi-section-table-cell { + display: inline-block; + flex: 10 10 auto; + flex-basis: 50%; + text-align: center; + } + .td.cbi-section-actions { + vertical-align: bottom; + } + .tr.table-titles, + .tr.cbi-section-table-titles, + .tr.cbi-section-table-descr { + display: none; + } + .tr[data-title]::before, + .tr.cbi-section-table-titles.named::before { + font-size: 0.9rem; + display: block; + flex: 1 1 100%; + border-bottom: thin solid rgba(0, 0, 0, 0.26); + background: #e9ecef; + } + .td[data-title], + [data-page^="admin-status-realtime"] .td[id] { + text-align: left; + } + .td[data-title]::before { + display: block; + } + .cbi-button + .cbi-button { + margin-left: 0; + } + .td.cbi-section-actions > * > *, + .td.cbi-section-actions > * > form > * { + margin: 2.1px 3px; + } + .Firewall form { + position: static !important; + margin: 0 0 2rem 0; + padding: 2rem; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + .Firewall form input { + width: 100% !important; + margin: 0; + margin-top: 1rem; + } + .Firewall .center, + .Firewall .center::before { + text-align: left !important; + } + .btn:not(button), + .cbi-button { + font-size: 0.875rem; + } +} +@media screen and (max-width: 768px) { + body { + font-size: 0.875rem; + } + .cbi-progressbar::after { + font-size: 0.6rem; + } + .main-left { + position: fixed; + z-index: 100; + width: 0; + } + .main-left.active { + width: 13rem; + } + .main-right { + width: 100%; + } + .main-right.active { + overflow-y: hidden; + } + .darkMask.active { + display: block; + } + .showSide { + padding: 0.1rem; + position: relative; + z-index: 99; + display: inline-block !important; + visibility: visible !important; + width: auto !important; + margin: 0 !important; + } + .showSide::before { + font-family: 'noobwrt' !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e20e"; + font-size: 1.7rem; + } + header > .fill > .container > .flex1 > .brand { + display: inline-block; + } + .main > .main-left > .nav > .slide > .slide-menu > li > a { + font-size: 0.875rem; + } +} +@media screen and (max-width: 600px) { + .mobile-hide { + display: none; + } + #maincontent > .container { + margin: 0 1rem 1rem 1rem; + } + .cbi-value-title { + text-align: left; + } + .cbi-dynlist p { + padding: 0.5rem 1rem; + } + body { + overflow-x: hidden; + } + .node-main-login .main .main-right #maincontent .container .cbi-map .cbi-section .cbi-section-node .cbi-value .cbi-value-field { + width: 16rem; + } + .node-main-login footer { + display: none; + } + .tabs::-webkit-scrollbar, + .cbi-tabmenu::-webkit-scrollbar { + width: 0px; + height: 0px; + } + .cbi-value-field, + .cbi-value-description { + display: block !important; + padding-left: 0 !important; + padding-right: 0 !important; + } + [data-page="admin-system-admin-password"] .cbi-value-field { + display: table-cell !important; + } + .modal.cbi-modal { + max-width: 100%; + max-height: none; + } + .modal { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 100%; + min-width: 270px; + max-width: 600px; + min-height: 32px; + margin: 5em auto; + padding: 1em; + border-radius: 3px !important; + background: #fff; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + .cbi-dropdown[open] > ul.dropdown { + margin-bottom: 1rem; + } + .login-page .login-container footer { + display: none; + } +} +@media screen and (max-width: 480px) { + .mobile-hide { + display: none; + } + div[style*="display:grid;grid-template-columns:repeat"] .ifacebox { + flex-basis: 80px; + } + div[style*="display:grid;grid-template-columns:repeat"] .ifacebox .ifacebox-body { + padding: 0.875rem 0.5rem; + font-size: 0.6rem; + } + .login-page .login-container { + margin-left: 0rem !important; + width: 100%; + padding: 1.5rem 1rem; + } + .login-page .login-container .login-form { + max-width: 22rem; + } + .login-page .login-container .login-form .login-card { + padding: 0; + } + .login-page .login-container .login-form .login-card.split { + grid-template-columns: 1fr; + } + .login-page .login-container .login-form .login-card .login-hero { + padding: 2rem 1.5rem 1.2rem; + } + .login-page .login-container .login-form .login-card .login-hero .hero-orbit { + display: none; + } + .login-page .login-container .login-form .login-card .login-hero .hero-particles span { + width: 6px; + height: 6px; + box-shadow: none; + } + .login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(2), + .login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(4), + .login-page .login-container .login-form .login-card .login-hero .hero-particles span:nth-child(12) { + width: 8px; + height: 8px; + } + .login-page .login-container .login-form .login-card .login-hero .hero-icon { + width: 7.5rem; + height: 7.5rem; + border-radius: 50%; + } + .login-page .login-container .login-form .login-card .login-hero .hero-icon img { + width: 5.1rem; + height: 5.1rem; + } + .login-page .login-container .login-form .login-card .login-hero .hero-brand { + max-width: 9rem; + } + .login-page .login-container .login-form .login-card .login-panel { + padding: 1.8rem 1.5rem 1.5rem; + } + .login-page .login-container .login-form .form-login .input-group::before { + color: var(--primary); + } +} +@media screen and (min-width: 600px) { + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + ::-webkit-scrollbar, + ::-webkit-scrollbar-corner { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 10px; + } + ::-webkit-scrollbar-thumb:hover { + background: var(--primary); + } + ::-webkit-scrollbar-thumb:active { + background: var(--primary); + } +} diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/css/dark.css b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/css/dark.css new file mode 100644 index 000000000..18ce6c4d4 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/css/dark.css @@ -0,0 +1,963 @@ +/** + * NoobWrt is a clean HTML5 theme for LuCI. It is based on luci-theme-material and NoobWrt Template + * + * luci-theme-noobwrt + * Copyright 2025 Jerryk + * + * Have a bug? Please create an issue here on GitHub! + * https://github.com/jerrykuku/luci-theme-noobwrt/issues + * + * luci-theme-bootstrap: + * Copyright 2008 Steven Barth + * Copyright 2008 Jo-Philipp Wich + * Copyright 2012 David Menting + * + * MUI: + * https://github.com/muicss/mui + * + * luci-theme-material: + * https://github.com/LuttyYang/luci-theme-material/ + * + * Agron Theme + * https://demos.creative-tim.com/argon-dashboard/index.html + * + * Login background + * https://unsplash.com/ + * + * Licensed to the public under the Apache License 2.0 + */ +body { + background: #1e1e1e; + color: #cccccc; +} +.spinning::before { + filter: invert(1); +} +.login-page .login-container .login-form .login-card { + background-color: rgba(18, 18, 28, 0.92); + -webkit-backdrop-filter: blur(var(--blur-radius-dark)) brightness(0.9); + backdrop-filter: blur(var(--blur-radius-dark)) brightness(0.9); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45); +} +.login-page .login-container .login-form .login-card .login-hero { + background: linear-gradient(135deg, rgba(33, 33, 44, 0.95), rgba(26, 27, 40, 0.95)); +} +.login-page .login-container .login-form .login-card .login-hero .hero-particles span { + background: rgba(124, 118, 255, 0.4); + box-shadow: 0 0 18px rgba(124, 118, 255, 0.45); +} +.login-page .login-container .login-form .login-card .login-hero .hero-icon { + background: rgba(124, 118, 255, 0.08); + border: 1px solid rgba(124, 118, 255, 0.28); + box-shadow: 0 0 28px rgba(124, 118, 255, 0.35), inset 0 0 18px rgba(124, 118, 255, 0.12); +} +.login-page .login-container .login-form .brand { + color: #adb5bd; +} +.login-page .login-container .login-form .brand-text-only { + color: #f3f4f6; +} +.login-page .login-container .login-form .login-subtitle { + color: #b7bdcc; + font-weight: 500; +} +.login-page .login-container .login-form .form-login .errorbox { + color: #fecaca; + background: rgba(220, 38, 38, 0.15); + border: 1px solid rgba(248, 113, 113, 0.3); +} +.login-page .login-container .login-form .form-login .input-group::before { + color: #a1a8b8; +} +.login-page .login-container .login-form .form-login .input-group input { + background-color: rgba(30, 30, 40, 0.9) !important; + color: #e5e7eb; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + border-radius: 999px !important; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 20px rgba(0, 0, 0, 0.35); +} +.login-page .login-container .login-form .form-login .input-group input:focus { + border-color: rgba(99, 102, 241, 0.65) !important; + background-color: rgba(36, 36, 48, 0.95) !important; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.25); +} +.login-page .login-container .login-form .form-login .input-group input::placeholder { + color: #8f96a8; +} +.login-page .login-container .login-form .form-login .cbi-button-apply { + background: linear-gradient(135deg, #7c76ff, #5b8dff) !important; +} +.login-page .login-container .login-form .form-login .cbi-button-apply:hover, +.login-page .login-container .login-form .form-login .cbi-button-apply:focus { + opacity: 0.95; +} +header::after { + background-color: #1e1e1e !important; +} +.main .main-left { + background-color: #333333 !important; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.15); +} +.main .main-left .sidenav-header .brand { + color: #cccccc; +} +.main .main-left .nav .slide .slide-menu .active a { + color: #cccccc; +} +.main .main-left .nav .slide .slide-menu .active a::after { + background-color: #cccccc !important; +} +.main .main-left .nav .slide .slide-menu .active::after { + background-color: var(--dark-primary) !important; +} +.main .main-left .nav .slide .slide-menu li a { + color: #cccccc; +} +.main .main-left .nav .slide .slide-menu li a:hover { + background: none !important; +} +.main .main-left .nav .slide .slide-menu li::after { + background-color: var(--dark-primary) !important; +} +.main .main-left .nav .slide .menu.active { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + color: #ffffff !important; +} +.main .main-left .nav .slide .menu.active a::after { + background-color: #ffffff !important; +} +.main .main-left .nav li a { + color: #cccccc !important; +} +.main .main-left .nav li a:hover { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + color: #ffffff !important; +} +.main .main-left::-webkit-scrollbar-thumb { + background-color: #252526 !important; +} +.main .main-left::-webkit-scrollbar-track { + background-color: #333; +} +.main .main-right { + background-color: #1e1e1e; +} +h2 { + color: #ccc; + background: #333333; +} +h3 { + color: #ccc; + border-bottom: 0; + background: #333333; +} +h4 { + color: #8C6900; +} +abbr { + color: var(--dark-primary) !important; +} +a:link, +a:visited, +a:active { + color: #a5b2ff; +} +input:-webkit-autofill { + background-color: #3c3c3c !important; +} +#channel_graph { + background-color: transparent !important; +} +.cbi-value-field .cbi-input-apply, +.cbi-button-apply, +.cbi-button-edit { + color: #fff !important; + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + border-color: #483d8b !important; + border-color: var(--dark-primary) !important; +} +.cbi-section em { + color: #ccc; +} +header.bg-primary { + background-color: #1e1e1e !important; +} +.cbi-map-descr { + color: #ccc; +} +.cbi-section { + background: none; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); +} +.panel-title { + color: #ccc; + background-color: #333333; +} +div > table > tbody > tr:nth-of-type(2n), +div > .table > .tr:nth-of-type(2n) { + background-color: #252526; +} +table > tbody > tr > td, +table > tfoot > tr > td, +table > thead > tr > td { + color: #cccccc; +} +fieldset > table > tbody > tr:nth-of-type(2n) { + background-color: #252526; +} +table > tbody > tr > td, +table > tfoot > tr > td, +table > thead > tr > td { + border-top: 1px solid #252526; +} +#swaptotal > div > div, +#swapfree > div > div, +#memfree > div > div, +#membuff > div > div, +#conns > div > div, +#memtotal > div > div { + background-color: var(--dark-primary) !important; +} +#swaptotal > div > div > div > small, +#swapfree > div > div > div > small, +#memfree > div > div > div > small, +#membuff > div > div > div > small, +#conns > div > div > div > small, +#memtotal > div > div > div > small { + color: #ccc !important; +} +.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last { + line-height: 1.8em; +} +.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"] { + border: 1px solid #999999 !important; + background-color: transparent !important; +} +.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"] div { + background-color: #ba8b00 !important; +} +table > tbody > tr > th, +table > tfoot > tr > th, +table > thead > tr > th { + background-color: #252526; + border-bottom: black 1px solid !important; +} +.tr[data-title]::before, +.tr.cbi-section-table-titles.named::before { + background-color: #252526; +} +tr > td, +tr > th, +.tr > .td, +.tr > .th, +.cbi-section-table-row::before, +#cbi-wireless > #wifi_assoclist_table > .tr:nth-child(2) { + border-top: 0; +} +.cbi-rowstyle-2 { + background-color: #1e1e1e; +} +.cbi-rowstyle-1 { + background-color: #252526; +} +.cbi-rowstyle-2 .cbi-button-up, +.cbi-rowstyle-2 .cbi-button-down, +body:not(.Interfaces) .cbi-rowstyle-2:first-child { + background-color: #666666 !important; +} +.cbi-section > h3:first-child, +.panel-title, +h3 { + color: #ccc; +} +h4 { + background-color: #1e1e1f; +} +.cbi-progressbar { + background: #282a2c; +} +.cbi-progressbar div { + background-color: var(--dark-primary) !important; +} +.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell { + background-color: #1e1e1f; +} +.cbi-button { + color: #ffffff !important; + background-color: #483d8b; + background-color: var(--dark-primary); +} +.cbi-section-node { + background: none; +} +abbr { + color: #5e72e4; +} +div > table > tbody > tr:nth-of-type(2n), +div > .table > .tbody > .tr:nth-of-type(2n) { + background-color: #252526; +} +#content_syslog { + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); +} +#syslog { + color: #ccc; + background-color: #1e1e1e; +} +#iwsvg, +#iwsvg2, +#bwsvg { + overflow: hidden; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); + background-color: #1e1e1e !important; +} +.tabs { + background-color: #252526; +} +.tabs li[class~="active"], +.tabs li:hover { + cursor: pointer; + color: #ccc !important; +} +.tabs li[class~="active"] a, +.tabs li:hover a { + color: #ccc !important; +} +.tabs > li[class~="active"] > a { + color: #ccc; +} +.tabs > li[class~="active"], +.tabs > li:hover { + border-bottom: 0.18751rem solid #483d8b; + border-bottom: 0.18751rem solid var(--dark-primary); + color: var(--dark-primary) !important; + background-color: #181819; +} +.cbi-tabmenu > li > a, +.tabs > li > a { + color: #ccc; +} +.cbi-tabmenu > li > a:hover, +.tabs > li > a:hover { + color: #ccc; +} +.cbi-tabmenu > li { + background: #2d2d2d; +} +.cbi-tabmenu li[class~="cbi-tab"] a { + color: #ccc; +} +.cbi-tabmenu > li:hover { + color: #ccc; + background: #2d2d2d; + border-bottom: 0.18751rem solid var(--dark-primary) !important; +} +.cbi-tabmenu > li[class~="cbi-tab"] { + background-color: #181819; + border-bottom: 0.18751rem solid var(--dark-primary) !important; +} +.cbi-tabcontainer > .cbi-value:nth-of-type(2n) { + background-color: #252526; +} +.cbi-value-title { + color: #ccc; +} +select, +input { + color: #ccc; + background-color: transparent !important; + border: 1px solid #252526; + box-shadow: none; +} +select:not([multiple="multiple"]):focus, +input:focus { + border-color: #483d8b !important; + border-color: var(--dark-primary) !important; + outline: 0; +} +select:not([multiple="multiple"]):focus, +input:not(.cbi-button):focus, +.cbi-dropdown:focus { + border-color: #5e72e4; + border-color: var(--dark-primary) !important; +} +select { + background-color: #1e1e1e !important; +} +#cbi-dropbear h2, +#cbi-dropbear .cbi-map-descr, +#cbi-dropbear .cbi-map-descr abbr, +#cbi-rc h2, +#cbi-rc .cbi-map-descr, +#cbi-distfeedconf h2, +#cbi-distfeedconf .cbi-map-descr, +#cbi-customfeedconf h2, +#cbi-customfeedconf .cbi-map-descr, +#cbi-download h2, +#cbi-filelist h2 { + color: #ccc !important; +} +.cbi-value-field > ul > li .ifacebadge { + background-color: #3c3c3c; +} +.cbi-section-descr { + color: #ccc; +} +.cbi-input-textarea { + background-color: #1e1e1e; + color: #ccc; + border: 1px solid #3c3c3c !important; +} +textarea:focus-visible { + border: 1px solid var(--dark-primary); +} +.cbi-section-remove:nth-of-type(2n), +.cbi-section-node:nth-of-type(2n) { + background-color: #1e1e1e; +} +.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3), +.cbi-section[id] .cbi-section-node:nth-of-type(4n+4) { + background-color: #1e1e1e; +} +.node-system-packages > .main table tr td:nth-last-child(1) { + color: #ccc; +} +.cbi-tooltip { + background-color: var(--primary); + color: #fff; +} +.ifacebox { + background-color: transparent !important; + border: 1px solid #1e1e1e; +} +.ifacebox-head { + color: #ccc; + background: #666; +} +.ifacebox-body { + background-color: #333; +} +.zonebadge strong { + color: #333; +} +.zonebadge > .ifacebadge { + background-color: #3c3c3c; +} +div.cbi-value var, +td.cbi-value-field var { + color: #5e72e4; +} +#diag-rc-output > pre { + color: #ccc; + background-color: #1e1e1e; +} +.node-services-vssr .block { + background-color: #3c3c3c !important; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); +} +.node-services-vssr .block h4 { + color: #ccc !important; +} +.node-services-vssr .status-bar { + color: #ccc; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); + background-color: #1e1e1e; +} +.node-services-vssr .cbi-section-table-row { + color: #ccc; + background-color: #3c3c3c !important; + box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.35); +} +.node-services-vssr .cbi-section-table-row.fast { + background: #483d8b !important; + background: var(--dark-primary) !important; + color: #fff; +} +.node-services-vssr .ssr-button { + color: #ccc; +} +.node-services-vssr .incon:nth-child(2) { + border-right: #1e1e1e 1px solid; +} +#xhr_poll_status > .label.success { + color: #ccc !important; + background-color: darkolivegreen !important; +} +.notice { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; +} +.cbi-section.fade-in .cbi-title > div:last-child span { + background: none!important; + color: #ced4da; +} +.cbi-input-find, +.cbi-input-save, +.cbi-button-add, +.cbi-button-save, +.cbi-button-find, +.cbi-input-reload, +.cbi-button-reload { + color: #fff !important; + background: darkolivegreen !important; + border-color: darkolivegreen !important; +} +.cbi-button-reset, +.cbi-input-remove { + color: #fff !important; + background-color: darkorange !important; + border-color: darkorange !important; +} +.cbi-page-actions .cbi-button-apply, +.cbi-section-actions .cbi-button-edit, +.cbi-button-edit.important, +.cbi-button-apply.important, +.cbi-button-reload.important, +.cbi-button-action.important { + border: 1px #483d8b solid !important; + border: 1px var(--dark-primary) solid !important; +} +.btn[value="Dismiss"], +.cbi-button[value="Terminate"], +.cbi-button[value="Reset"], +.cbi-button[value="Disabled"], +.cbi-button[onclick^="iface_reconnect"], +.cbi-button[onclick="handleReset(event)"], +.cbi-button-neutral[value="Disable"] { + color: #fff; + border: thin solid darkorange !important; + background-color: darkorange !important; +} +fieldset[id^="cbi-apply-"] { + background-color: #333333; +} +#detail-bubble > div { + border: 1px solid #ccc; + background: #252525; +} +.ifacebox-head.active { + background-color: var(--dark-primary) !important; +} +header .fill .status span[data-style="active"] { + color: #ccc !important; + background-color: darkolivegreen !important; +} +#cbi-wireless .td, +#cbi-network .tr:first-child > .td, +.table[width="100%"] > .tr:first-child > .td, +[data-page="admin-network-diagnostics"] .tr > .td, +.tr.table-titles > .th, +.tr.cbi-section-table-titles > .th { + background-color: #252526; + border-bottom: black 1px solid !important; +} +.network-status-table .ifacebox-body .ifacebadge { + background-color: #252526; + border-bottom: 0; + box-shadow: none; +} +td > .ifacebadge, +.td > .ifacebadge { + color: #fff; + background-color: #483d8b; + background-color: var(--dark-primary); + border: 0; +} +.alert, +.alert-message { + background-color: #333; +} +.alert-message.warning { + background-color: #986400 !important; +} +.alert.error, +.alert-message.error { + background-color: #784f00; +} +.alert h4, +.alert-message h4 { + background-color: #784f00; + color: #ffbf00; +} +.alert-message [class="btn"] { + background-color: #777777; + color: #ccc; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > ins { + border-color: #00ad00; + background-color: #688668; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > del { + border-color: #c60000; + background-color: #896565; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var { + background-color: #333; + border-color: #666; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var ins { + background-color: #688668; +} +.uci-dialog .cbi-section .uci-change-legend .uci-change-legend-label > var del { + background-color: #896565; +} +.uci-dialog .cbi-section .uci-change-list > var { + background-color: #333; + border-color: #666; +} +.uci-dialog .cbi-section .uci-change-list > var del { + background-color: #896565; +} +.uci-dialog .cbi-section .uci-change-list > var ins { + background-color: #688668; +} +.uci-dialog .cbi-section .uci-change-list > ins { + border-color: #00ad00; + background-color: #688668; +} +.uci-dialog .cbi-section .uci-change-list > del { + border-color: #c60000; + background-color: #896565; +} +.uci-dialog .cbi-section .uci-change-list + .right .btn { + color: #ccc; +} +.uci-dialog .cbi-section .uci-change-list + .right .cbi-button { + border: 1px solid #3c3c3c !important; +} +.btn.danger, +.cbi-section-remove > .cbi-button, +.cbi-button-remove, +.cbi-button-reset, +.cbi-button-negative, +.cbi-button[value="Stop"], +.cbi-button[value="Kill"], +.cbi-button[onclick="reboot(this)"], +.cbi-button-neutral[value="Restart"] { + border: thin solid darkorange !important; + background-color: darkorange !important; +} +.cbi-section, +.cbi-section-error, +#iptables, +.Firewall form, +#cbi-network > .cbi-section-node, +#cbi-wireless > .cbi-section-node, +#cbi-wireless > #wifi_assoclist_table, +[data-tab-title], +[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), +[data-page="admin-system-opkg"] #maincontent > .container { + background: #1e1e1e !important; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); +} +div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] { + background: transparent !important; +} +[data-page="admin-system-admin"] .cbi-map h2, +[data-page="admin-system-admin-password"] .cbi-map h2, +[data-page="admin-system-admin"] .cbi-map .cbi-map-descr, +[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr { + color: #ccc; +} +[data-page="admin-system-flash"] .modal label > input[type="checkbox"] { + vertical-align: text-top; + top: auto; + border-color: #fff !important; +} +[data-page="admin-system-flash"] .modal .btn { + white-space: normal !important; + background-color: darkseagreen; +} +[data-page="admin-system-flash"] .modal .alert-message { + background-color: transparent !important; +} +[data-page="admin-system-flash"] .modal .danger { + border: thin solid darkorange !important; + background-color: darkorange !important; +} +.cbi-value input[type="password"] + .cbi-button-neutral { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + border-color: var(--dark-primary) !important; +} +.btn, +button, +select, +input, +.cbi-dropdown { + border: 1px solid #3c3c3c !important; +} +.cbi-dropdown .preview { + color: #ccc; +} +.cbi-section-table-row { + background-color: #1e1e1e !important; +} +.modal { + background-color: #1e1e1e; +} +.cbi-button-positive { + color: #fff !important; + background-color: darkolivegreen !important; +} +[data-page="admin-system-flash"] legend { + color: #ccc; +} +.logout:before { + color: #adb5bd !important; +} +.cbi-dropdown[open] { + border-color: #483d8b !important; + border-color: var(--dark-primary) !important; +} +.cbi-dropdown[open] > ul.dropdown { + background: #252526 !important; + color: #ffffff !important; + box-shadow: none; + border: 1px solid #3c3c3c !important; +} +.cbi-dropdown[open] > ul.dropdown li { + color: #ffffff; + border-bottom: 1px solid #3c3c3c !important; +} +.cbi-dropdown[open] > ul.dropdown > li[selected] { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + border-bottom: 1px solid #3c3c3c !important; +} +.cbi-dropdown[open] > ul.dropdown > li.focus { + background: #483d8b; + background: var(--dark-primary); + outline: none; +} +.ifacebadge { + background-color: #333333; +} +.cbi-dynlist > .item > span { + border: 1px solid #3c3c3c !important; +} +.cbi-page-actions .cbi-button-apply, +.cbi-section-actions .cbi-button-edit, +.cbi-button-edit, +.cbi-button-apply, +.cbi-button-reload, +.cbi-button-action, +.cbi-button[value="Submit"], +.cbi-button[value="Upload"], +.cbi-button[value$="Apply"], +.cbi-button[onclick="addKey(event)"] { + background: #483d8b !important; + background: var(--dark-primary) !important; + border-color: var(--dark-primary) !important; +} +.btn.primary, +.cbi-page-actions .cbi-button-save, +.cbi-page-actions .cbi-button-apply + .cbi-button-save, +.cbi-button-add, +.cbi-button-save, +.cbi-button-positive, +.cbi-button-link, +.cbi-button[value="Enable"], +.cbi-button[value="Scan"], +.cbi-button[value^="Back"], +.cbi-button-neutral[onclick="handleConfig(event)"] { + background: #483d8b; + background: var(--dark-primary); +} +[data-page="admin-system-opkg"] h2 { + color: #ccc !important; +} +[data-page="admin-system-startup"] textarea { + color: #ccc; + background-color: transparent; +} +[data-page="admin-system-startup"] textarea:focus-visible { + border: 1px solid var(--dark-primary); +} +[data-page="admin-network-firewall-custom"] #view p textarea, +[data-page="admin-status-routes"] #view p textarea, +[data-page="admin-system-crontab"] #view p textarea { + color: #ccc; + background-color: transparent; +} +#view > .spinning { + background: #333333 !important; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03) !important; +} +@media screen and (min-width: 600px) { + ::-webkit-scrollbar-thumb { + background: var(--dark-primary); + } + ::-webkit-scrollbar-thumb:hover { + background: var(--dark-primary); + } + ::-webkit-scrollbar-thumb:active { + background: var(--dark-primary); + } +} +@media screen and (max-width: 480px) { + .node-status-iptables > .main div > .cbi-map > form { + background-color: #1e1e1e; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); + } +} +[data-page=""] .main-right > #maincontent .Dashboard, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard { + color: #ccc !important; +} +[data-page=""] .main-right > #maincontent .Dashboard h3, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard h3 { + color: #ccc; +} +[data-page=""] .main-right > #maincontent .Dashboard hr, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard hr { + border-top: 1px solid #ffffff; +} +[data-page=""] .main-right > #maincontent .Dashboard .dashboard-bg, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard .dashboard-bg { + background-color: #333333; +} +[data-page=""] .main-right > #maincontent .Dashboard img.svgmonochrome, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard img.svgmonochrome { + filter: invert(90%); +} +[data-page=""] .main-right > #maincontent .Dashboard tr, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard tr { + border-top: thin solid #4d4d4d; +} +[data-page=""] .main-right > #maincontent .Dashboard tr:last-child, +[data-page="admin-dashboard"] .main-right > #maincontent .Dashboard tr:last-child { + border-bottom: thin solid #4d4d4d; +} +[data-page="admin-system-fileassistant"] .fb-container .panel-container { + border-bottom-color: #3c3c3c !important; +} +[data-page="admin-system-fileassistant"] .fb-container td[class$="-icon"]::before { + filter: invert(0.7); +} +[data-page="admin-system-fileassistant"] div#list-content table.cbi-section-table tbody tr:nth-child(1) td.parent-icon { + background: #1e1e1e !important; +} +[data-page^="admin-services-openclash"] .oc { + --bg-light: #1e1e1e !important; + --bg-gray: #1e1e1e !important; +} +[data-page^="admin-services-openclash"] .oc .developer-container { + background: transparent !important; +} +[data-page^="admin-services-openclash"] .config-upload-content { + background: #1e1e1e !important; +} +[data-page^="admin-services-openclash"] .cbi-tabmenu::-webkit-scrollbar-thumb { + background-color: #5b5b5b; +} +[data-page^="admin-services-openclash"] .cbi-tabmenu::-webkit-scrollbar-track { + background-color: rgba(60, 60, 60, 0.75); +} +[data-page^="admin-services-openclash"] #tab { + border-color: #3c3c3c; +} +[data-page^="admin-services-openclash"] #diag-rc-output > pre, +[data-page^="admin-services-openclash"] #dns-rc-output > pre { + color: #ccc; + border: 1px solid #3c3c3c !important; + background-color: #1e1e1e; +} +[data-page^="admin-services-openclash"] img[src$="arrow-clockwise-light.svg"], +[data-page^="admin-services-openclash"] img[src$="wrench-light.svg"], +[data-page^="admin-services-openclash"] img[src$="eye-light.svg"] { + filter: invert(0.9); +} +[data-page="admin-network-diagnostics"] .cbi-section { + background: #252526 !important; +} +[data-page="admin-network-diagnostics"] textarea { + background: transparent; + border-radius: 0.25rem; + color: #ccc; + border: 1px solid #3c3c3c !important; +} +[data-page="admin-network-diagnostics"] .tr > .td { + background-color: #252526 !important; +} +[data-page="admin-network-network"] .ifacebox-head[style*="--zone-color-rgb: 144, 240, 144"] { + background-color: #497e49 !important; + color: #ccc !important; +} +[data-page="admin-network-network"] .ifacebox-head[style*="--zone-color-rgb: 240, 144, 144;"] { + background-color: #9c4f4f !important; + color: #ccc !important; +} +[data-page="admin-network-network"] .ifacebox-head[style*="--zone-color-rgb: 238, 238, 238;"] { + background-color: #666 !important; +} +[data-page="admin-network-network"] #modal_overlay > .modal.cbi-modal > div > p > textarea { + border: 1px solid #3c3c3c !important; + background-color: transparent !important; +} +[data-page="admin-network-network"] #modal_overlay div[data-tab="peers"] div[data-name="_peers"] .ifacebadge code { + color: #fff !important; + background-color: transparent !important; +} +[data-page="admin-network-network"] #modal_overlay > .modal.cbi-modal > div > p > textarea { + color: #ccc !important; +} +[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] { + --zone-color-rgb: 156, 79, 79 !important; +} +[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] strong { + color: #ccc !important; +} +[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] { + --zone-color-rgb: 112, 112, 112 !important; +} +[data-page="admin-network-firewall-rules"] #cbi-firewall-rule .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] em { + color: #ccc !important; +} +[data-page="admin-network-firewall-rules"] #cbi-firewall-rule > table > .cbi-section-table-row[data-title]::before { + background-color: #252526 !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"], +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] { + --zone-color-rgb: 156, 79, 79 !important; + color: #ccc !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] strong, +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] strong { + color: #ccc !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"], +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] { + --zone-color-rgb: 73, 126, 73 !important; + color: #ccc !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] strong, +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] strong { + color: #ccc !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"], +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] { + --zone-color-rgb: 112, 112, 112 !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong, +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong { + color: #ccc !important; +} +[data-page="admin-network-network"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong + span, +[data-page="admin-network-firewall-rules"] .cbi-dropdown .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] strong + span { + color: #ccc !important; +} +[data-page="admin-system-commands"] .commandbox, +[data-page="admin-system-commands-dashboard"] .commandbox { + border-bottom: thin solid #333; + background: #252526; + box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.2), 0 1px 2px rgba(255, 255, 255, 0.05); +} +.btn { + background-color: #707070; + color: #fff; +} diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/favicon.ico b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/favicon.ico new file mode 100644 index 000000000..9a64ea362 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/favicon.ico differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/GoogleSans-Regular.woff b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/GoogleSans-Regular.woff new file mode 100644 index 000000000..ddd21225b Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/GoogleSans-Regular.woff differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/GoogleSans-Regular.woff2 b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/GoogleSans-Regular.woff2 new file mode 100644 index 000000000..64b6ac6a7 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/GoogleSans-Regular.woff2 differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/RedHatDisplay-Italic-Variable.woff2 b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/RedHatDisplay-Italic-Variable.woff2 new file mode 100644 index 000000000..319d5c57c Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/RedHatDisplay-Italic-Variable.woff2 differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/RedHatDisplay-Variable.woff2 b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/RedHatDisplay-Variable.woff2 new file mode 100644 index 000000000..3bba15e75 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/RedHatDisplay-Variable.woff2 differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/TypoGraphica.woff b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/TypoGraphica.woff new file mode 100644 index 000000000..fef38ca00 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/TypoGraphica.woff differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/TypoGraphica.woff2 b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/TypoGraphica.woff2 new file mode 100644 index 000000000..2b8ff0cb3 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/TypoGraphica.woff2 differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/noobwrt.woff b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/noobwrt.woff new file mode 100644 index 000000000..ef1e9c2c4 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/noobwrt.woff differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/noobwrt.woff2 b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/noobwrt.woff2 new file mode 100644 index 000000000..f38c5242e Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/fonts/noobwrt.woff2 differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/android-icon-192x192.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/android-icon-192x192.png new file mode 100644 index 000000000..c0a6c2f5f Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/android-icon-192x192.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-144x144.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-144x144.png new file mode 100644 index 000000000..ec7732a40 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-144x144.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-60x60.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-60x60.png new file mode 100644 index 000000000..c93850af0 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-60x60.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-72x72.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-72x72.png new file mode 100644 index 000000000..e6ea41587 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/apple-icon-72x72.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/arrow.svg b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/arrow.svg new file mode 100644 index 000000000..a20e7987d --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/browserconfig.xml b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/browserconfig.xml new file mode 100644 index 000000000..c55414822 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/browserconfig.xml @@ -0,0 +1,2 @@ + +#ffffff \ No newline at end of file diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-16x16.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-16x16.png new file mode 100644 index 000000000..43bc80810 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-16x16.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-32x32.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-32x32.png new file mode 100644 index 000000000..42587a733 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-32x32.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-96x96.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-96x96.png new file mode 100644 index 000000000..57c73e7e9 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/favicon-96x96.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/manifest.json b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/manifest.json new file mode 100644 index 000000000..d4b84f159 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "Openwrt", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/ms-icon-144x144.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/ms-icon-144x144.png new file mode 100644 index 000000000..ec7732a40 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/ms-icon-144x144.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/spinner.svg b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/spinner.svg new file mode 100644 index 000000000..caef4b7ac --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/icon/spinner.svg @@ -0,0 +1,3 @@ + + + diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/bg1.jpg b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/bg1.jpg new file mode 100644 index 000000000..7e587113a Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/bg1.jpg differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/blank.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/blank.png new file mode 100644 index 000000000..9f4100e6e Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/blank.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/brand.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/brand.png new file mode 100644 index 000000000..15dee7d94 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/brand.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/logo.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/logo.png new file mode 100644 index 000000000..b8a84bdd0 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/logo.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/noobwrt.svg b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/noobwrt.svg new file mode 100644 index 000000000..b983da176 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/noobwrt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/volume_high.svg b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/volume_high.svg new file mode 100644 index 000000000..ef84c3c6e --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/volume_high.svg @@ -0,0 +1,3 @@ + + + diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/volume_off.svg b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/volume_off.svg new file mode 100644 index 000000000..5467d4d57 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/img/volume_off.svg @@ -0,0 +1,3 @@ + + + diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/cell.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/cell.png new file mode 100644 index 000000000..27b266758 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/cell.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/files.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/files.png new file mode 100644 index 000000000..375678322 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/files.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/firewall.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/firewall.png new file mode 100644 index 000000000..0560c4235 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/firewall.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/home.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/home.png new file mode 100644 index 000000000..12cade259 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/home.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/info.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/info.png new file mode 100644 index 000000000..94fa8da60 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/info.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/nas.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/nas.png new file mode 100644 index 000000000..5f17faa15 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/nas.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/network.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/network.png new file mode 100644 index 000000000..2c2c685df Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/network.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/settings.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/settings.png new file mode 100644 index 000000000..19c95201e Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/settings.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/signal.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/signal.png new file mode 100644 index 000000000..8c40b3dfe Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/signal.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/sms.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/sms.png new file mode 100644 index 000000000..5c7daa894 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/sms.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/terminal.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/terminal.png new file mode 100644 index 000000000..0fbf0ff65 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/terminal.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/vpn.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/vpn.png new file mode 100644 index 000000000..610beadbe Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/vpn.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/wifi.png b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/wifi.png new file mode 100644 index 000000000..81adc87e9 Binary files /dev/null and b/luci-theme-noobwrt/htdocs/luci-static/noobwrt/navbar/wifi.png differ diff --git a/luci-theme-noobwrt/htdocs/luci-static/resources/menu-noobwrt.js b/luci-theme-noobwrt/htdocs/luci-static/resources/menu-noobwrt.js new file mode 100644 index 000000000..c8cb39997 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/resources/menu-noobwrt.js @@ -0,0 +1,514 @@ +'use strict'; +'require baseclass'; +'require ui'; + +/** + * Native JavaScript slide animation utilities + * Replaces jQuery slideUp/slideDown functionality with better performance + */ +const SlideAnimations = { + /** + * Animation durations in milliseconds. + * When prefers-reduced-motion is active, all animations use 0ms. + */ + durations: { + fast: 200, + normal: 400, + slow: 600 + }, + + /** + * Returns true if the user has requested reduced motion via OS/browser setting. + */ + prefersReducedMotion: function() { + return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + }, + + /** + * Map to track running animations and their cleanup functions + */ + runningAnimations: new WeakMap(), + + /** + * Slide element down (show) with animation + * @param {Element} element - DOM element to animate + * @param {string|number} duration - Animation duration ('fast', 'normal', 'slow' or milliseconds) + * @param {function} callback - Optional callback function when animation completes + */ + slideDown: function(element, duration, callback) { + if (!element) { + console.warn('SlideAnimations.slideDown: No element provided'); + return; + } + + // Stop any existing animation on this element + this.stop(element); + + // Convert duration string to milliseconds; skip animation for reduced-motion users + const animDuration = this.prefersReducedMotion() ? 0 : + (typeof duration === 'string' ? + this.durations[duration] || this.durations.normal : + (duration || this.durations.normal)); + + // Store original styles + const originalStyles = { + display: element.style.display, + overflow: element.style.overflow, + height: element.style.height, + transition: element.style.transition + }; + + // Set initial state for animation + element.style.display = 'block'; + element.style.overflow = 'hidden'; + element.style.height = '0px'; + element.style.transition = `height ${animDuration}ms ease-out`; + + // Force reflow to ensure initial state is applied + element.offsetHeight; + + // Get the target height + const targetHeight = element.scrollHeight; + + // Animate to full height + element.style.height = targetHeight + 'px'; + + // Set up cleanup function + const cleanup = () => { + element.style.height = originalStyles.height || ''; + element.style.overflow = originalStyles.overflow || ''; + element.style.transition = originalStyles.transition || ''; + + // Remove from running animations map + this.runningAnimations.delete(element); + + if (callback && typeof callback === 'function') { + try { + callback.call(element); + } catch (e) { + console.error('SlideAnimations callback error:', e); + } + } + }; + + // Store cleanup function for potential cancellation + const timeoutId = setTimeout(cleanup, animDuration); + this.runningAnimations.set(element, { timeoutId, cleanup }); + }, + + /** + * Slide element up (hide) with animation + * @param {Element} element - DOM element to animate + * @param {string|number} duration - Animation duration ('fast', 'normal', 'slow' or milliseconds) + * @param {function} callback - Optional callback function when animation completes + */ + slideUp: function(element, duration, callback) { + if (!element) { + console.warn('SlideAnimations.slideUp: No element provided'); + return; + } + + // Stop any existing animation on this element + this.stop(element); + + // Convert duration string to milliseconds; skip animation for reduced-motion users + const animDuration = this.prefersReducedMotion() ? 0 : + (typeof duration === 'string' ? + this.durations[duration] || this.durations.normal : + (duration || this.durations.normal)); + + // Store original styles + const originalStyles = { + display: element.style.display, + overflow: element.style.overflow, + height: element.style.height, + transition: element.style.transition + }; + + // Get current height before hiding + const currentHeight = element.scrollHeight; + + // Set initial state for animation + element.style.overflow = 'hidden'; + element.style.height = currentHeight + 'px'; + element.style.transition = `height ${animDuration}ms ease-out`; + + // Force reflow to ensure initial state is applied + element.offsetHeight; + + // Animate to zero height + element.style.height = '0px'; + + // Set up cleanup function + const cleanup = () => { + element.style.display = 'none'; + element.style.height = originalStyles.height || ''; + element.style.overflow = originalStyles.overflow || ''; + element.style.transition = originalStyles.transition || ''; + + // Remove from running animations map + this.runningAnimations.delete(element); + + if (callback && typeof callback === 'function') { + try { + callback.call(element); + } catch (e) { + console.error('SlideAnimations callback error:', e); + } + } + }; + + // Store cleanup function for potential cancellation + const timeoutId = setTimeout(cleanup, animDuration); + this.runningAnimations.set(element, { timeoutId, cleanup }); + }, + + /** + * Stop all running animations on an element + * @param {Element} element - DOM element to stop animations on + */ + stop: function(element) { + if (!element) return; + + const animationData = this.runningAnimations.get(element); + if (animationData) { + // Clear the timeout + clearTimeout(animationData.timeoutId); + + // Run cleanup immediately + animationData.cleanup(); + } + + // Clear transition to immediately stop any CSS animation + element.style.transition = ''; + + // Force reflow to apply changes immediately + element.offsetHeight; + }, + + /** + * Check if element has running animation + * @param {Element} element - DOM element to check + * @returns {boolean} - True if element has running animation + */ + isAnimating: function(element) { + return this.runningAnimations.has(element); + } +}; + +/** + * NoobWrt Theme Menu Module + * Handles rendering and interaction of the main navigation menu and sidebar + */ +return baseclass.extend({ + /** + * Initialize the menu module + * Load menu data and trigger rendering + */ + __init__: function () { + ui.menu.load().then(L.bind(this.render, this)); + }, + + /** + * Main render function for the menu system + * @param {Object} tree - Menu tree structure from LuCI + */ + render: function (tree) { + var node = tree; + var url = ''; + + this.renderModeMenu(node); + + // Render tab menu if we're deep enough in the navigation hierarchy + if (L.env.dispatchpath.length >= 3) { + for (var i = 0; i < 3 && node; i++) { + node = node.children[L.env.dispatchpath[i]]; + url = url + (url ? '/' : '') + L.env.dispatchpath[i]; + } + + if (node) { + this.renderTabMenu(node, url); + } + } + + // Attach event listeners for sidebar toggle functionality + var sidebarToggle = document.querySelector('a.showSide'); + var darkMask = document.querySelector('.darkMask'); + + if (sidebarToggle) { + sidebarToggle.addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle')); + } + if (darkMask) { + darkMask.addEventListener('click', ui.createHandlerFn(this, 'handleSidebarToggle')); + } + }, + + /** + * Handle menu expand/collapse functionality + * Manages the sliding animation and active states of menu items + * @param {Event} ev - Click event from menu item + */ + handleMenuExpand: function (ev) { + var target = ev.target; + var slide = target.parentNode; + var slideMenu = target.nextElementSibling; + var shouldCollapse = false; + + // Close all currently active submenus + var activeMenus = document.querySelectorAll('.main .main-left .nav > li > ul.active'); + activeMenus.forEach(function (ul) { + // Stop any running animations and slide up + SlideAnimations.stop(ul); + // Remove active classes immediately when starting slideUp animation + ul.classList.remove('active'); + ul.previousElementSibling.classList.remove('active'); + SlideAnimations.slideUp(ul, 'fast'); + + // Check if we're clicking on an already open menu (should collapse it) + if (!shouldCollapse && ul === slideMenu) { + shouldCollapse = true; + } + }); + + // Persist open section: clear since we just closed everything + try { + localStorage.setItem('noobwrt.sidenav.open', '[]'); + } catch (e) {} + + // Exit if there's no submenu to show + if (!slideMenu) { + return; + } + + // Open the submenu if it's not already open + if (!shouldCollapse) { + // Find the slide menu within the slide element + var slideMenuElement = slide.querySelector(".slide-menu"); + if (slideMenuElement) { + // Add active classes immediately when starting slideDown animation + slideMenu.classList.add('active'); + target.classList.add('active'); + SlideAnimations.slideDown(slideMenuElement, 'fast'); + + // Persist which section is open + try { + var openTitle = target.getAttribute('data-title'); + if (openTitle) { + localStorage.setItem('noobwrt.sidenav.open', JSON.stringify([openTitle])); + } + } catch (e) {} + } + target.blur(); // Remove focus from the clicked element + } + + // Prevent default link behavior and event bubbling + ev.preventDefault(); + ev.stopPropagation(); + }, + + /** + * Render the main navigation menu + * Creates hierarchical menu structure with active states and click handlers + * @param {Object} tree - Menu tree node to render + * @param {string} url - Base URL for menu items + * @param {number} level - Current nesting level (0-based) + * @returns {Element} - Generated menu element + */ + renderMainMenu: function (tree, url, level) { + var currentLevel = (level || 0) + 1; + var menuContainer = E('ul', { 'class': level ? 'slide-menu' : 'nav' }); + var children = ui.menu.getChildren(tree); + + // Don't render empty menus or menus deeper than 2 levels + if (children.length === 0 || currentLevel > 2) { + return E([]); + } + + // Generate menu items for each child + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var isActive = ( + (L.env.dispatchpath[currentLevel] === child.name) && + (L.env.dispatchpath[currentLevel - 1] === tree.name) + ); + + // Recursively render submenu + var submenu = this.renderMainMenu(child, url + '/' + child.name, currentLevel); + var hasChildren = submenu.children.length > 0; + + // Determine CSS classes based on state + var slideClass = hasChildren ? 'slide' : null; + var menuClass = hasChildren ? 'menu' : 'food'; + + if (isActive) { + menuContainer.classList.add('active'); + slideClass += " active"; + menuClass += " active"; + } + + // Create menu item with link and submenu + var menuItem = E('li', { 'class': slideClass }, [ + E('a', { + 'href': L.url(url, child.name), + 'click': (currentLevel === 1) ? ui.createHandlerFn(this, 'handleMenuExpand') : null, + 'class': menuClass, + 'data-title': child.title.replace(/ /g, "_"), // More robust space replacement + }, [_(child.title)]), + submenu + ]); + + menuContainer.appendChild(menuItem); + } + + // Append to main menu container if this is the top level + if (currentLevel === 1) { + var mainMenuElement = document.querySelector('#mainmenu'); + if (mainMenuElement) { + mainMenuElement.appendChild(menuContainer); + mainMenuElement.style.display = ''; + + // Restore previously opened submenus from localStorage + var openSections = []; + try { + openSections = JSON.parse(localStorage.getItem('noobwrt.sidenav.open') || '[]'); + } catch (e) {} + + menuContainer.querySelectorAll('.nav > li.slide > a.menu').forEach(function(anchor) { + var title = anchor.getAttribute('data-title'); + var slideMenu = anchor.nextElementSibling; + if (!slideMenu) return; + + var isActive = anchor.classList.contains('active'); + var wasOpen = openSections.indexOf(title) !== -1; + + if (isActive || wasOpen) { + anchor.classList.add('active'); + slideMenu.classList.add('active'); + slideMenu.style.display = 'block'; + } + }); + + // Scroll the active menu item into view smoothly + var activeItem = mainMenuElement.querySelector('.nav > li > a.active, .nav > li > a.menu.active'); + if (activeItem) { + setTimeout(function() { + activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, 150); + } + } + } + + return menuContainer; + }, + + renderModeMenu: function (tree) { + var menu = document.querySelector('#modemenu'); + var children = ui.menu.getChildren(tree); + + for (var i = 0; i < children.length; i++) { + var isActive = (L.env.requestpath.length ? children[i].name == L.env.requestpath[0] : i == 0); + if (i > 0) + menu.appendChild(E([], ['\u00a0|\u00a0'])); + menu.appendChild(E('li', {}, [ + E('a', { + 'href': L.url(children[i].name), + 'class': isActive ? 'active' : null + }, [_(children[i].title)]) + ])); + if (isActive) + this.renderMainMenu(children[i], children[i].name); + } + if (menu.children.length > 1) + menu.style.display = ''; + }, + + /** + * Render tab navigation menu + * Creates horizontal tab menu for deeper navigation levels + * @param {Object} tree - Menu tree node to render + * @param {string} url - Base URL for tab items + * @param {number} level - Current nesting level (0-based) + * @returns {Element} - Generated tab menu element + */ + renderTabMenu: function (tree, url, level) { + var container = document.querySelector('#tabmenu'); + var currentLevel = (level || 0) + 1; + var tabContainer = E('ul', { 'class': 'tabs' }); + var children = ui.menu.getChildren(tree); + var activeNode = null; + + // Don't render empty tab menus + if (children.length === 0) { + return E([]); + } + + // Generate tab items for each child + for (var i = 0; i < children.length; i++) { + var child = children[i]; + var isActive = (L.env.dispatchpath[currentLevel + 2] === child.name); + var activeClass = isActive ? ' active' : ''; + var className = 'tabmenu-item-%s %s'.format(child.name, activeClass); + + var tabItem = E('li', { 'class': className }, [ + E('a', { 'href': L.url(url, child.name) }, [_(child.title)]) + ]); + + tabContainer.appendChild(tabItem); + + // Store reference to active node for recursive rendering + if (isActive) { + activeNode = child; + } + } + + // Append tab container to main tab menu element + if (container) { + container.appendChild(tabContainer); + container.style.display = ''; + + // Recursively render nested tab menus if there's an active node + if (activeNode) { + var nestedTabs = this.renderTabMenu(activeNode, url + '/' + activeNode.name, currentLevel); + if (nestedTabs.children.length > 0) { + container.appendChild(nestedTabs); + } + } + } + + return tabContainer; + }, + + /** + * Handle sidebar toggle functionality + * Toggles the mobile/responsive sidebar menu visibility + * @param {Event} ev - Click event from sidebar toggle button or dark mask + */ + handleSidebarToggle: function (ev) { + var showSideButton = document.querySelector('a.showSide'); + var sidebar = document.querySelector('#mainmenu'); + var darkMask = document.querySelector('.darkMask'); + var scrollbarArea = document.querySelector('.main-right'); + + // Check if any required elements are missing + if (!showSideButton || !sidebar || !darkMask || !scrollbarArea) { + console.warn('Sidebar toggle elements not found'); + return; + } + + // Toggle sidebar visibility and related states + if (showSideButton.classList.contains('active')) { + // Close sidebar + showSideButton.classList.remove('active'); + sidebar.classList.remove('active'); + scrollbarArea.classList.remove('active'); + darkMask.classList.remove('active'); + } else { + // Open sidebar + showSideButton.classList.add('active'); + sidebar.classList.add('active'); + scrollbarArea.classList.add('active'); + darkMask.classList.add('active'); + } + } +}); + diff --git a/luci-theme-noobwrt/htdocs/luci-static/resources/view/noobwrt/settings.js b/luci-theme-noobwrt/htdocs/luci-static/resources/view/noobwrt/settings.js new file mode 100644 index 000000000..37f1dacd8 --- /dev/null +++ b/luci-theme-noobwrt/htdocs/luci-static/resources/view/noobwrt/settings.js @@ -0,0 +1,306 @@ +'use strict'; +'require view'; +'require form'; +'require uci'; +'require rpc'; +'require ui'; + +var callSetCustomWallpaper = rpc.declare({ + object: 'luci.noobwrt_wallpaper', + method: 'set_custom', + expect: { result: -1 } +}); + +var callRemoveCustomWallpaper = rpc.declare({ + object: 'luci.noobwrt_wallpaper', + method: 'remove_custom', + expect: { result: -1 } +}); + +return view.extend({ + load: function () { + return uci.load('noobwrt').catch(function () { + /* /etc/config/noobwrt is absent (fresh install or missing defaults). + * Seed an in-memory named section 'global' so form.NamedSection + * can render without firing uci/get RPCs that return code 4. + * uci.add() returns the internal sid; setting .name promotes it + * to a named section that form.NamedSection('global') will find. */ + var sid = uci.add('noobwrt', 'global'); + uci.set('noobwrt', sid, '.name', 'global'); + uci.set('noobwrt', sid, 'mode', 'normal'); + uci.set('noobwrt', sid, 'primary', '#5e72e4'); + uci.set('noobwrt', sid, 'dark_primary', '#7c8ff5'); + uci.set('noobwrt', sid, 'blur', '10'); + uci.set('noobwrt', sid, 'blur_dark', '10'); + uci.set('noobwrt', sid, 'transparency', '0.8'); + uci.set('noobwrt', sid, 'transparency_dark', '0.8'); + uci.set('noobwrt', sid, 'online_wallpaper', 'none'); + uci.set('noobwrt', sid, 'auto_dark_mode', '0'); + }); + }, + + render: function () { + var m, s, o; + + m = new form.Map('noobwrt', _('NoobWrt Theme Settings'), + _('Customize the appearance, wallpaper, background effects, and toolbar panel of the NoobWrt LuCI theme.')); + + /* ============================================================ + * SECTION: Global – tabbed (Appearance / Wallpaper / Effects / Toolbar / About) + * ============================================================ */ + s = m.section(form.NamedSection, 'global', 'global'); + s.addremove = false; + s.anonymous = true; + + s.tab('appearance', _('Appearance')); + s.tab('wallpaper', _('Wallpaper')); + s.tab('effects', _('Background Effects')); + s.tab('toolbar', _('Toolbar Panel')); + s.tab('about', _('About')); + + /* ---- Appearance ---- */ + o = s.taboption('appearance', form.Flag, 'auto_dark_mode', + _('Automatic Light / Dark Mode'), + _('Automatically switch to Dark mode from 6 PM to 6 AM and Light mode from 6 AM to 6 PM. When enabled, the schedule overrides the manual toolbar toggle.')); + o.default = '1'; + + o = s.taboption('appearance', form.ListValue, 'mode', + _('Default Theme Mode'), + _('Theme applied on page load when Automatic mode is disabled. Users can also toggle it manually using the toolbar sun/moon button.')); + o.value('normal', _('Light')); + o.value('dark', _('Dark')); + + o = s.taboption('appearance', form.Value, 'primary', + _('Accent Color — Light Mode'), + _('Primary accent color used throughout the light theme.')); + o.placeholder = '#5e72e4'; + o.rmempty = false; + o.renderWidget = function(section_id, option_index, cfgvalue) { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + var input = node.querySelector('input'); + if (input) { + input.style.cssText = 'width:140px;display:inline-block;vertical-align:middle;margin-right:6px'; + var picker = document.createElement('input'); + picker.type = 'color'; + picker.value = (cfgvalue && cfgvalue !== this.placeholder) ? cfgvalue : this.placeholder; + picker.style.cssText = 'width:40px;height:32px;padding:2px;border:1px solid #ccc;border-radius:4px;cursor:pointer;vertical-align:middle'; + picker.addEventListener('input', function() { input.value = picker.value; input.dispatchEvent(new Event('input', { bubbles: true })); }); + input.addEventListener('input', function() { if (/^#[0-9a-fA-F]{6}$/.test(input.value)) picker.value = input.value; }); + node.appendChild(picker); + } + return node; + }; + + o = s.taboption('appearance', form.Value, 'dark_primary', + _('Accent Color — Dark Mode'), + _('Primary accent color used throughout the dark theme.')); + o.placeholder = '#7c8ff5'; + o.rmempty = false; + o.renderWidget = function(section_id, option_index, cfgvalue) { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + var input = node.querySelector('input'); + if (input) { + input.style.cssText = 'width:140px;display:inline-block;vertical-align:middle;margin-right:6px'; + var picker = document.createElement('input'); + picker.type = 'color'; + picker.value = (cfgvalue && cfgvalue !== this.placeholder) ? cfgvalue : this.placeholder; + picker.style.cssText = 'width:40px;height:32px;padding:2px;border:1px solid #ccc;border-radius:4px;cursor:pointer;vertical-align:middle'; + picker.addEventListener('input', function() { input.value = picker.value; input.dispatchEvent(new Event('input', { bubbles: true })); }); + input.addEventListener('input', function() { if (/^#[0-9a-fA-F]{6}$/.test(input.value)) picker.value = input.value; }); + node.appendChild(picker); + } + return node; + }; + + /* ---- Wallpaper ---- */ + var BGURL = '/luci-static/noobwrt/background/custom.jpg'; + var BGTMP = '/tmp/noobwrt_wallpaper.tmp'; + + o = s.taboption('wallpaper', form.ListValue, 'online_wallpaper', + _('Online Wallpaper Source'), + _('Automatically fetch a wallpaper from an online source for the login page background. Set to None to use a custom uploaded image or the built-in default.')); + o.value('none', _('None (use custom / default)')); + o.value('bing', _('Bing Daily Photo')); + o.value('picsum', _('Lorem Picsum (Random)')); + o.value('wikimedia', _('Wikimedia Picture of the Day')); + o.default = 'none'; + + o = s.taboption('wallpaper', form.DummyValue, '_wallpaper_ui', ''); + o.rawhtml = true; + o.default = [ + '
', + '

', + _('Upload a custom image to use as the login page background. Supported formats: JPG, PNG, WebP (max 10 MB).'), + '

', + '
', + '', + '' + _('No custom wallpaper — using default') + '', + '
', + '

', + '
', + '', + '', + '
', + '
' + ].join(''); + + + /* ---- Background Effects ---- */ + o = s.taboption('effects', form.Value, 'blur', + _('Blur Radius — Light Mode'), + _('Login page background blur radius in pixels for light mode. Default: 10')); + o.placeholder = '10'; + o.datatype = 'uinteger'; + + o = s.taboption('effects', form.Value, 'blur_dark', + _('Blur Radius — Dark Mode'), + _('Login page background blur radius in pixels for dark mode. Default: 10')); + o.placeholder = '10'; + o.datatype = 'uinteger'; + + o = s.taboption('effects', form.Value, 'transparency', + _('Background Opacity — Light Mode'), + _('Overlay opacity on the login page background for light mode. Range: 0.0 (transparent) to 1.0 (opaque). Default: 0.8')); + o.placeholder = '0.8'; + + o = s.taboption('effects', form.Value, 'transparency_dark', + _('Background Opacity — Dark Mode'), + _('Overlay opacity on the login page background for dark mode. Default: 0.8')); + o.placeholder = '0.8'; + + /* ---- Toolbar tab – placeholder only, real table rendered below ---- */ + o = s.taboption('toolbar', form.DummyValue, '_toolbar_placeholder', ''); + o.rawhtml = true; + o.default = '
'; + + /* ---- About ---- */ + o = s.taboption('about', form.DummyValue, '_about', ''); + o.rawhtml = true; + o.default = [ + '
', + '

' + _('NoobWrt Theme') + '

', + '

' + _('A clean modern LuCI theme for OpenWrt routers.') + '

', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + '
' + _('Developer') + 'NoobLk
' + _('Repository') + 'github.com/nooblk-98/luci-theme-noobwrt
' + _('Contributors') + '' + _('View on GitHub') + '
' + _('License') + 'MIT
' + _('Issues / Support') + '' + _('Open an issue') + '
', + '

' + _('Made with \u2665 for the OpenWrt community') + '

', + '
' + ].join('\n'); + + /* ============================================================ + * SECTION: Toolbar Panel Items (TableSection) + * Rendered separately, then moved into the Toolbar tab via JS. + * ============================================================ */ + var ts = m.section(form.TableSection, 'toolbar_item', + _('Toolbar Panel Items'), + _('Quick-access links displayed in the right-side toolbar panel on every page. Items are rendered in ascending Order number. Use the Add button below to create new entries.')); + ts.addremove = true; + ts.anonymous = true; + ts.sortable = false; + ts.nodescriptions = true; + + o = ts.option(form.Flag, 'enabled', _('Enable')); + o.default = '1'; + o.editable = true; + + o = ts.option(form.Value, 'title', _('Label')); + o.placeholder = 'Home'; + o.rmempty = false; + o.editable = true; + + o = ts.option(form.Value, 'url', _('URL / Path')); + o.placeholder = '/cgi-bin/luci/admin/status/overview'; + o.rmempty = false; + o.editable = true; + + o = ts.option(form.ListValue, 'icon', _('Icon')); + [ + ['home.png', _('Home')], + ['signal.png', _('Signal / Status')], + ['cell.png', _('Cell / Modem')], + ['sms.png', _('SMS')], + ['network.png', _('Network / Data Usage')], + ['nas.png', _('NAS / Storage')], + ['wifi.png', _('WiFi / Wireless')], + ['firewall.png', _('Firewall')], + ['settings.png', _('Settings / System')], + ['terminal.png', _('Terminal')], + ['vpn.png', _('VPN')], + ['files.png', _('Files')], + ['info.png', _('Info / Log')] + ].forEach(function (v) { o.value(v[0], v[1]); }); + o.editable = true; + + o = ts.option(form.Value, 'order', + _('Order'), + _('Lower numbers appear first in the toolbar.')); + o.datatype = 'uinteger'; + o.placeholder = '10'; + o.editable = true; + + return m.render().then(function (node) { + /* ---- Wire wallpaper buttons ---- */ + var uploadBtn = node.querySelector('#noobwrt-wp-upload'); + var revertBtn = node.querySelector('#noobwrt-wp-revert'); + var wpImg = node.querySelector('#noobwrt-wp-img'); + var wpPh = node.querySelector('#noobwrt-wp-placeholder'); + var wpSt = node.querySelector('#noobwrt-wp-status'); + + function setStatus(msg, ok) { + wpSt.textContent = msg; + wpSt.style.color = ok === true ? '#2dce89' : ok === false ? '#f5365c' : '#888'; + } + + fetch(BGURL, { method: 'HEAD' }) + .then(function (r) { + if (r.ok) { wpImg.src = BGURL + '?_=' + Date.now(); wpImg.style.display = ''; wpPh.style.display = 'none'; } + }).catch(function () {}); + + uploadBtn.addEventListener('click', function (ev) { + ui.uploadFile(BGTMP, ev.target) + .then(function () { return callSetCustomWallpaper(); }) + .then(function (result) { + if (result === 0) { + wpImg.src = BGURL + '?_=' + Date.now(); wpImg.style.display = ''; wpPh.style.display = 'none'; + setStatus(_('Wallpaper uploaded! Refresh the login page to see it.'), true); + } else { + setStatus(_('Failed to save wallpaper.'), false); + } + }) + .catch(function (e) { setStatus(_('Upload error: ') + e.message, false); }); + }); + + revertBtn.addEventListener('click', function () { + if (!window.confirm(_('Remove custom wallpaper and revert to default?'))) return; + callRemoveCustomWallpaper() + .then(function () { + wpImg.src = ''; wpImg.style.display = 'none'; wpPh.style.display = ''; + setStatus(_('Reverted to default wallpaper.'), true); + }) + .catch(function (e) { setStatus(_('Could not remove: ') + e.message, false); }); + }); + + /* ---- Move TableSection into Toolbar tab ---- */ + var tabContent = node.querySelector('#noobwrt-toolbar-tab-content'); + var sections = node.querySelectorAll('.cbi-map > .cbi-section'); + var tsNode = sections[sections.length - 1]; + + if (tabContent && tsNode) { + var heading = tsNode.querySelector('h3, .cbi-section-legend'); + if (heading) heading.style.display = 'none'; + tabContent.appendChild(tsNode); + } + + return node; + }); + } +}); diff --git a/luci-theme-noobwrt/less/cascade.less b/luci-theme-noobwrt/less/cascade.less new file mode 100644 index 000000000..2f9b9a999 --- /dev/null +++ b/luci-theme-noobwrt/less/cascade.less @@ -0,0 +1,2518 @@ +// out: ../htdocs/luci-static/noobwrt/css/cascade.css, compress: false , sourceMap: false +/** + * NoobWrt is a clean HTML5 theme for LuCI. It is based on luci-theme-material and NoobWrt Template + * + * luci-theme-noobwrt + * Copyright 2025 Jerrykuku + * + * Have a bug? Please create an issue here on GitHub! + * https://github.com/jerrykuku/luci-theme-noobwrt/issues + * + * luci-theme-material: + * Copyright 2015 Lutty Yang + * https://github.com/LuttyYang/luci-theme-material/ + * + * Agron Theme + * https://demos.creative-tim.com/argon-dashboard/index.html + * + * Login background + * https://unsplash.com/ + * Font generate by Icomoon + * + * Licensed to the public under the Apache License 2.0 + */ + +:root { + --primary: #5e72e4; + --dark-primary: #483d8b; + --header-color: #fff; + --menu-bg-color: #ffffff; + --menu-color: #5f6368; + --main-menu-color: #202124; + --red: #f5365c; + --orange: #fb6340; + --yellow: #ffd600; + --green: #2dce89; + --teal: #11cdef; + --cyan: #2bffc6; + --white: #fff; + --gray: #8898aa; + --gray-dark: #32325d; + --light: #adb5bd; + --lighter: #e9ecef; + + --success: #2dce89; + --info: #11cdef; + --warning: #fb6340; + --danger: #f5365c; + + --dark: #212529; + --default: #172b4d; + --darker: black; + --background-color: #f4f5f7; + --login-form-bg-color: rgba(244, 245, 247, 0.8); + --blur-radius: 10px; + --blur-opacity: 0.5; + --blur-radius-dark: 10px; + --blur-opacity-dark: 0.5; + --font-family-sans-serif: "Red Hat Display", "Microsoft Yahei", "WenQuanYi Micro Hei", "sans-serif", "Helvetica Neue", "Helvetica", "Hiragino Sans GB"; + --dropdown-arrow-icon: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik01LjI5MyA5LjI5M2ExIDEgMCAwIDEgMS40MTQgMEwxMiAxNC41ODZsNS4yOTMtNS4yOTNhMSAxIDAgMSAxIDEuNDE0IDEuNDE0bC02IDZhMSAxIDAgMCAxLTEuNDE0IDBsLTYtNmExIDEgMCAwIDEgMC0xLjQxNCIvPjwvc3ZnPg=="); +} + +/* + * Include base and custom css + */ +@import url("fonts.less"); +@import url("normalize.less"); +@import url("pure-min.less"); + +// table + +.table { + position: relative; + display: table; +} + +.tr { + display: table-row; +} + +.thead { + display: table-header-group; +} + +.tbody { + display: table-row-group; +} + +.tfoot { + display: table-footer-group; +} + +.td, +.th { + line-height: normal; + display: table-cell; + padding: .5em; + text-align: center; + vertical-align: middle; +} + +.th { + font-weight: bold; + white-space: nowrap; +} + +.tr.placeholder { + height: 4em; +} + +.tr.placeholder>.td { + line-height: 3; + position: absolute; + right: 0; + bottom: 0; + left: 0; + padding: 0.4rem 0 !important; + text-align: center !important; + background: inherit; +} + +.td[width="33%"] { + padding: 1.1em 1.5rem; +} + +.table[width="33%"], +.th[width="33%"], +.td[width="33%"] { + width: 33%; +} + +.table[width="100%"], +.th[width="100%"], +.td[width="100%"] { + width: 100%; +} + + +.btn, +button, +select, +input, +.cbi-dropdown { + line-height: 1.5rem; + height: 2.5rem; + padding: .5rem .75rem; + color: #8898aa; + border: 1px solid #dee2e6; + border-radius: .25rem; + outline: 0; + background-image: none; + box-shadow: none; + transition: box-shadow .15s ease; +} + + + +select { + padding-right: 1.5rem; + background-image: url("data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSIjODg5OGFhIiBkPSJNNS4yOTMgOS4yOTNhMSAxIDAgMCAxIDEuNDE0IDBMMTIgMTQuNTg2bDUuMjkzLTUuMjkzYTEgMSAwIDEgMSAxLjQxNCAxLjQxNGwtNiA2YTEgMSAwIDAgMS0xLjQxNCAwbC02LTZhMSAxIDAgMCAxIDAtMS40MTQiLz48L3N2Zz4="); + background-size: 1rem; + background-position: right 0.5rem center; + background-repeat: no-repeat; + appearance: none; +} + +select:not([multiple="multiple"]):focus, +input:not(.cbi-button):focus, +.cbi-dropdown:focus { + border-color: var(--primary); + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); + ; +} + +.cbi-dropdown, +select[multiple="multiple"] { + height: auto; +} + + +@import url("sysauth.less"); +@import url("layout.less"); + +#xhr_poll_status { + display: flex; + margin-left: 0.5rem; + + * { + color: #fff; + } +} + + +// alert + +.danger { + background-color: var(--danger) !important; +} + +.warning { + background-color: var(--warning) !important; +} + +.success { + background-color: var(--success) !important; +} + +.notice { + background-color: var(--info) !important; + color: #fff; +} + +.error { + color: var(--danger); +} + +.alert, +.alert-message { + position: fixed; + width: 20rem; + z-index: 9000; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + font-weight: bold; + padding: 1rem 1.25rem; + border: 0; + border-radius: 0.25rem !important; + background-color: #fff; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); + text-shadow: none; + + + &.error { + background-color: var(--warning); + } + + h4 { + padding: 0.25rem 0rem; + border-radius: 0.25rem; + } + + .btn { + height: auto; + } +} + +.modal.alert-message { + position: static; + width: 90%; + min-width: 270px; + max-width: 600px; + left: auto; + top: auto; + transform: none; + margin: 5em auto; +} + +.alert-message>h4 { + font-size: 110%; + font-weight: bold; +} + +.alert-message>* { + margin: .5rem 0; +} + +.alert-message .btn { + padding: .3rem .6rem; +} + +.container .alert, +.container .alert-message { + margin-left: 0; + margin-right: 0; + margin-top: 0rem; + position: relative; + top: 0; + transform:translate(-50%, 0); + width: 100%; +} + +#mwan3-service-status > .alert-message { + left: 0.5rem; + transform: none; +} + +.lg { + margin: 0; + padding: 0 !important; +} + +.cbi-section, +.cbi-section-error, +#iptables, +.Firewall form, +#cbi-network>.cbi-section-node, +#cbi-wireless>.cbi-section-node, +#cbi-wireless>#wifi_assoclist_table, +[data-tab-title], +[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), +[data-page="admin-system-opkg"] #maincontent>.container { + font-family: inherit; + font-weight: normal; + font-style: normal; + line-height: normal; + min-width: inherit; + padding: 0; + border: 0; + + border-radius: 0.25rem; + background-color: #fff; + box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .15); + + &:last-child { + margin: 0; + border: 0; + } + +} + +.cbi-modal .cbi-section, +.cbi-section .cbi-section { + padding: 0; + box-shadow: none; +} + +.cbi-modal .cbi-tabmenu { + margin-left: 0; +} + +.cbi-map { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.cbi-map>.cbi-tabmenu+div { + margin-top: -0.4375rem; +} + +.cbi-map-descr { + font-size: small; + line-height: 1.5; + padding: 0 1.25rem; +} + +.cbi-section { + + &>.cbi-section-descr { + padding-top: 1rem !important; + padding-bottom: 1rem !important; + + &:empty { + display: none; + } + } + +} + +.cbi-section-descr:not(:empty) { + font-size: small; + line-height: 1.5; + padding: 0rem 1rem; +} + +.cbi-section-descr:empty { + display: none !important; +} + +.cbi-map-descr+fieldset { + margin-top: 1rem; +} + +.cbi-map-descr>abbr { + cursor: help; + text-decoration: underline; +} + +.cbi-section>legend { + display: none !important; +} + +fieldset>fieldset, +.cbi-section>.cbi-section { + margin: 0; + padding: 0; + border: 0; + box-shadow: none; +} + +.cbi-section>h3:first-child, +.panel-title { + font-size: 1.1rem; + line-height: 1; + display: block; + width: 100%; + margin: 0; + margin-bottom: 0; + padding: 0.8755rem 1.25rem; + color: #32325d; + color: var(--gray-dark); + +} + +.cbi-section>h3:first-child, +.cbi-section>h4:first-child, +.cbi-section>p:first-child, +[data-tab-title]>h3:first-child, +[data-tab-title]>h4:first-child, +[data-tab-title]>p:first-child { + padding: 1rem 1.25rem; +} + +.cbi-section p { + padding: 1rem; +} + +.cbi-tblsection { + overflow-x: auto; +} + +table { + border-spacing: 0; + border-collapse: collapse; +} + +table, +.table { + overflow-y: hidden; + width: 100%; + font-size: 0.75rem; +} + +.table .table-titles th { + background-color: var(--lighter); +} + +table>tbody>tr>td, +table>tbody>tr>th, +table>tfoot>tr>td, +table>tfoot>tr>th, +table>thead>tr>td, +table>thead>tr>th, +.table>.tbody>.tr>.td, +.table>.tbody>.tr>.th, +.table>.tfoot>.tr>.td, +.table>.tfoot>.tr>.th, +.table>.thead>.tr>.td, +.table>.thead>.tr>.th, +.table>.tr>.td.cbi-value-field, +.table>.tr>.th.cbi-section-table-cell { + padding: .5rem; +} + +.container>.cbi-section:first-of-type>.table[width="100%"]>.tr>.td { + padding: .6rem; +} + +.cbi-section-table-cell { + line-height: 1.1; + align-self: flex-end; + flex: 1 1 auto; +} + +tr>td, +tr>th, +.tr>.td, +.tr>.th, +.cbi-section-table-row::before, +#cbi-wireless>#wifi_assoclist_table>.tr:nth-child(2) { + border-top: thin solid #ddd; + padding: 1.1em 1.25rem; +} + +#cbi-wireless .td, +.table[width="100%"]>.tr:first-child>.td, +[data-page="admin-network-diagnostics"] .tr>.td, +.tr.table-titles>.th, +.tr.cbi-section-table-titles>.th { + border-top: 0 !important; + background-color: #f6f9fc; + padding: 1.1em 1.25rem; + line-height: 1.3rem; +} + + + + + +#cbi-network .tr:first-child>.td { + border-top: 0; +} + +.table[width="100%"]>.tr:first-child>.td { + margin: auto 0; +} + +.cbi-section-table-row { + margin-bottom: 1rem; + text-align: center !important; + background: #f4f4f4; +} + +.cbi-section-table-row:last-child { + margin-bottom: 0; +} + +.cbi-section-table-row>.cbi-value-field .cbi-dropdown, +.cbi-section-table-row>.cbi-value-field .cbi-input-select, +.cbi-section-table-row>.cbi-value-field .cbi-input-text, +.cbi-section-table-row>.cbi-value-field .cbi-input-password { + width: 100%; +} + +.cbi-section-table-row>.cbi-value-field .cbi-input-text, +.cbi-section-table-row>.cbi-value-field .cbi-input-password { + min-width: 80px; +} + +.cbi-section-table-row>.cbi-value-field [data-dynlist]>input, +.cbi-section-table-row>.cbi-value-field input.cbi-input-password { + width: calc(100% - 1.5rem); +} + +.cbi-section-table-row .td { + text-align: center !important; + + .cbi-checkbox { + input[type="checkbox"] { + margin: 0; + } + } +} + +.control-group { + display: inline-flex; + width: 100%; + flex-wrap: wrap; + gap: 1rem; + + input { + border-right-width: 0; + margin-right: 0; + } + + input+button { + border-bottom-left-radius: 0; + border-top-left-radius: 0; + margin-left: 0; + border-left-width: 0; + } +} + +.control-group:has(> input:first-child + .cbi-button) { + gap: 0 !important; + + input { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + width: 15.5rem; + min-width: 15.5rem; + } +} + + + +.control-group>* { + vertical-align: middle; +} + +div>table>tbody>tr:nth-of-type(2n), +div>.table>.tr:nth-of-type(2n) { + background-color: #f9f9f9; +} + +/* fix multiple table */ +table table, +.table .table, +.cbi-value-field table, +.cbi-value-field .table, +td>table>tbody>tr>td, +.td>.table>.tbody>.tr>.td, +.cbi-value-field>table>tbody>tr>td, +.cbi-value-field>.table>.tbody>.tr>.td { + border: 0; +} + +/* button style */ +.btn, +.cbi-button, +.item::after { + font-size: .875rem; + display: inline-block; + width: auto !important; + padding: 0.5rem .75rem; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + transition: all .2s ease-in-out; + text-align: center; + vertical-align: middle; + white-space: nowrap; + text-decoration: none; + border: 0; + border-radius: .25rem; + background-color: #f0f0f0; + background-image: none; + appearance: none; + -ms-touch-action: manipulation; + touch-action: manipulation; +} + + +.btn:not(button) ul:not(.dropdown) li { + padding: 0; +} + +.cbi-button-up, +.cbi-button-down { + display: inline-block; + min-width: 0; + padding: .2rem 1rem; + font-size: 0; + color: transparent !important; + background: url(../icon/arrow.svg) no-repeat center; + background-size: 12px 20px; +} + +.cbi-button-up { + transform: scaleY(-1); +} + +.cbi-button:not(select) { + appearance: none !important; +} + +.btn:hover, +.btn:focus, +.btn:active, +.cbi-button:hover, +.cbi-button:focus, +.cbi-button:active, +.item:hover::after, +.item:focus::after, +.item:active::after, +.cbi-page-actions .cbi-button-apply+.cbi-button-save:hover, +.cbi-page-actions .cbi-button-apply+.cbi-button-save:focus, +.cbi-page-actions .cbi-button-apply+.cbi-button-save:active { + text-decoration: none; + outline: 0; +} + +.btn:hover, +.btn:focus, +.cbi-button:hover, +.cbi-button:focus, +.item:hover::after, +.item:focus::after { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} + +.btn:active, +.cbi-button:active, +.item:active::after { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} + +.cbi-button-up:hover, +.cbi-button-up:focus { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} + +.cbi-button-up:active { + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); +} + +.btn:disabled, +.cbi-button:disabled { + cursor: not-allowed; + pointer-events: none; + opacity: .5; + box-shadow: none; +} + +/* gray */ +.alert-message [class="btn"], +.modal div[class="btn"], +.cbi-button-find, +.cbi-button-link, +.cbi-button-up, +.cbi-button-down, +.cbi-button-neutral, +.cbi-button[name="zero"], +.cbi-button[name="restart"], +.cbi-button[onclick="hide_empty(this)"] { + color: #fff; + border: thin solid #8898aa; + background-color: #8898aa; +} + +/* dark blue */ +.btn.primary, +.cbi-page-actions .cbi-button-save, +.cbi-page-actions .cbi-button-apply+.cbi-button-save, +.cbi-button-add, +.cbi-button-save, +.cbi-button-positive, +.cbi-button-link, +.cbi-button[value="Enable"], +.cbi-button[value="Scan"], +.cbi-button[value^="Back"], +.cbi-button-neutral[onclick="handleConfig(event)"] { + font-weight: normal; + color: #fff !important; + border: thin solid #5e72e4; + border: thin solid var(--primary); + background-color: #5e72e4; + background-color: var(--primary); + ; +} + +/* light blue */ +.cbi-page-actions .cbi-button-apply, +.cbi-section-actions .cbi-button-edit, +.cbi-button-edit, +.cbi-button-apply, +.cbi-button-reload, +.cbi-button-action, +.cbi-button[value="Submit"], +.cbi-button[value="Upload"], +.cbi-button[value$="Apply"], +.cbi-button[onclick="addKey(event)"] { + font-weight: normal; + color: #fff !important; + border: thin solid #5e72e4; + border: thin solid var(--primary); + + background-color: #5e72e4; + background-color: var(--primary); +} + +/* red */ +.btn.danger, +.cbi-section-remove>.cbi-button, +.cbi-button-remove, +.cbi-button-reset, +.cbi-button-negative, +.cbi-button[value="Stop"], +.cbi-button[value="Kill"], +.cbi-button[onclick="reboot(this)"], +.cbi-button-neutral[value="Restart"] { + font-weight: normal; + color: #fff; + border: thin solid #f5365c; + border: thin solid var(--red); + background-color: #f5365c; + background-color: var(--red); +} + +/* yellow */ +.btn[value="Dismiss"], +.cbi-button[value="Terminate"], +.cbi-button[value="Reset"], +.cbi-button[value="Disabled"], +.cbi-button[onclick^="iface_reconnect"], +.cbi-button[onclick="handleReset(event)"], +.cbi-button-neutral[value="Disable"] { + font-weight: normal; + color: var(--white); + border: thin solid #eea236; + background-color: #f0ad4e; +} + +/* green */ +.cbi-button-success, +.cbi-button-download { + font-weight: normal; + color: var(--white); + border: thin solid #4cae4c; + background-color: #5cb85c; +} + +.cbi-page-actions { + .cbi-button-link:first-child { + float: left; + } + + .cbi-dropdown { + .open { + color: var(--white); + } + + .more { + display: block; + width: 1px; + height: 100%; + overflow: hidden; + text-indent: 5000px; + background-color: var(--white) + } + } +} + + +.a-to-btn { + text-decoration: none; +} + +.cbi-value-field .cbi-button-add { + font-weight: bold; + padding: 1px 6px; + display: inline-block; + align-items: center; +} + +.tabs { + padding: 0 1rem; + background-color: #FFFFFF; + border-radius: 0.25rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, .03); + white-space: nowrap; + overflow-x: auto; + + &::-webkit-scrollbar { + width: 1px; + height: 5px; + } + + &::-webkit-scrollbar-thumb { + background-color: #f6f9fc + } + + &::-webkit-scrollbar-track { + background-color: #fff; + } + + li[class~="active"], + li:hover { + cursor: pointer; + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + color: #5e72e4; + color: var(--primary); + background-color: #e4e9ee; + margin-bottom: 0; + border-radius: 0; + + a { + color: #5e72e4; + color: var(--primary); + } + } + + li { + font-size: 0.875rem; + display: inline-block; + padding: 0.875rem 0rem; + border-bottom: 0.18751rem solid rgba(0, 0, 0, 0); + margin: 0; + transition: all 0.2s; + + a { + text-decoration: none; + color: #404040; + padding: 0.5rem 0.8rem; + } + + &:hover { + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + } + } +} + + +.cbi-tabmenu { + color: white; + padding: 0.5rem 1rem 0 1rem; + white-space: nowrap; + overflow-x: auto; + + &::-webkit-scrollbar { + width: 1px; + height: 5px; + } + + &::-webkit-scrollbar-thumb { + background-color: #f6f9fc + } + + &::-webkit-scrollbar-track { + background-color: #fff; + } + + li { + background: #dce3e9; + display: inline-block; + font-size: 0.875rem; + border-top-left-radius: 0.25rem; + border-top-right-radius: 0.25rem; + padding: 0.5rem 0rem; + border-bottom: 0.18751rem solid rgba(0, 0, 0, 0); + margin: 0 0.2rem; + position: relative; + + a { + text-decoration: none; + color: #404040; + padding: 0.5rem 0.8rem; + } + + &:hover { + cursor: pointer; + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + color: #5e72e4; + color: var(--primary); + background-color: #e4e9ee; + margin-bottom: 0; + + a { + color: #525f7f; + } + } + } + + li[class~="cbi-tab"] { + border-bottom: 0.18751rem solid #5e72e4; + border-bottom: 0.18751rem solid var(--primary); + color: #5e72e4; + color: var(--primary); + background-color: #e4e9ee; + margin-bottom: 0; + + a { + color: #5e72e4; + color: var(--primary); + } + } +} + +.cbi-tab-descr { + padding: 0 1.25rem; +} + +.cbi-section-node { + display: flex; + flex-direction: column; + gap: 0.875rem; + + >[id^="cbi-config-"] { + display: flex; + flex-direction: column; + gap: 0.875rem; + + &:first-child { + padding-top: 1rem; + } + + &:last-child { + padding-bottom: 1rem; + } + } +} + +.cbi-section .cbi-section-remove:nth-of-type(2n), +.container>.cbi-section .cbi-section-node:nth-of-type(2n) { + background-color: #f9f9f9; +} + +[data-tab-title] { + overflow: hidden; + height: 0; + opacity: 0; + margin: 0; + padding: 1rem 0; + display: none !important; + + p { + margin-left: 1rem; + margin-bottom: 1rem; + } +} + +[data-tab-active="true"] { + overflow: visible; + height: auto; + opacity: 1; + display: inherit !important; + transition: opacity .25s ease-in; + margin: inherit !important; +} + + +.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3), +.cbi-section[id] .cbi-section-node:nth-of-type(4n+4) { + background-color: #f9f9f9; +} + +.cbi-section-node-tabbed { + margin-top: 0; + border: 0 solid #d4d4d4; + border-radius: 0.25rem; + + &>div { + display: flex; + + flex-direction: column; + gap: 0.875rem; + } +} + +.cbi-tabcontainer>.cbi-value:nth-of-type(2n) { + background-color: #f9f9f9; +} + +.cbi-value-field { + display: table-cell; + + & div+br { + display: none; + } +} + +.cbi-value-description { + line-height: 1.25; + display: table-cell; + padding: .5rem 0; + opacity: .5; + font-size: 0.75rem; + + abbr { + color: #32325d; + color: var(--gray-dark); + } + + &:empty { + display: none; + } +} + +.cbi-value-title { + display: table-cell; + float: left; + width: 23rem; + + padding-right: 2rem; + text-align: right; + word-wrap: break-word; +} + +.cbi-value { + display: inline-block; + width: 100%; + padding: 0 1rem; + line-height: 2.4rem; + + ul { + line-height: 1.25; + } +} + +.cbi-value-field.cbi-dropdown-open { + .cbi-dropdown { + border-color: var(--primary); + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, .1); + } +} + +.cbi-value-field .cbi-dropdown, +.cbi-value-field .cbi-input-select, +.cbi-value input[type="text"], +.cbi-value input[type="password"], +.cbi-value textarea { + min-width: 18rem; +} + + + + +#cbi-firewall-zone .cbi-input-select, +#cbi-network-switch_vlan .cbi-input-select { + min-width: 11rem; +} + +#cbi-network-switch_vlan .cbi-input-text { + max-width: 3rem; +} + +.cbi-input-file { + appearance: auto !important; + + &::-webkit-file-upload-button { + display: inline-block !important; + } +} + +.cbi-input-invalid { + color: #f5365c !important; + border-color: #f5365c !important; +} + +.cbi-section-error { + font-weight: bold; + line-height: 1.42857143; + margin: 18px; + padding: 6px; + border: thin solid #f5365c; + border-radius: 3px; + background-color: #fce6e6; +} + +.cbi-section-error ul { + margin: 0 0 0 20px; +} + +.cbi-section-error ul li { + font-weight: bold; + color: #f5365c; +} + +.td[data-title]::before { + font-weight: bold; + display: none; + padding: .25rem 0; + content: attr(data-title) ":\20"; + text-align: left; + white-space: nowrap; +} + +.tr.placeholder .td[data-title]::before { + display: none; +} + +.tr[data-title]::before, +.tr.cbi-section-table-titles.named::before { + font-weight: bold; + display: table-cell; + align-self: center; + flex: 1 1 5%; + padding: .25rem; + content: attr(data-title) "\20"; + text-align: center; + vertical-align: middle; + white-space: normal; + word-wrap: break-word; + background-color: #f6f9fc; +} + +.cbi-rowstyle-1 { + background-color: #f9f9f9; +} + +.cbi-rowstyle-2 { + background-color: #eee; +} + +.cbi-rowstyle-2 .cbi-button-up, +.cbi-rowstyle-2 .cbi-button-down, +body:not(.Interfaces) .cbi-rowstyle-2:first-child { + background-color: #fff !important; +} + +.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell { + width: auto !important; +} + +.td.cbi-section-actions { + text-align: right !important; + vertical-align: middle; +} + +.td.cbi-section-actions>* { + display: inline-flex; + gap: 1rem; +} + +.td.cbi-section-actions>*>*, +.td.cbi-section-actions>*>form>* { + display: flex; + align-items: center; +} + +.td.cbi-section-actions>*>form { + display: inline-flex; + margin: 0; +} + +.td .cbi-checkbox { + justify-content: center; +} + +.cbi-checkbox { + display: flex; + align-items: center; + height: 2.5rem; +} + +/* lists */ +.cbi-dynlist { + + line-height: 1.3; + flex-direction: column; + min-height: 30px; + cursor: text; + gap: 0.875rem; + + &>.item { + display: inline-flex; + flex-wrap: nowrap; + position: relative; + max-width: 25rem; + pointer-events: none; + color: #8898aa; + outline: 0; + + &::after { + content: "\00D7"; + pointer-events: auto; + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem !important; + height: 2.5rem; + margin: 0; + font-weight: normal; + font-size: 1rem; + line-height: 1.5rem; + + color: #fff; + border: 1px solid #f5365c; + border-radius: 0 0.25rem 0.25rem 0; + outline: 0; + + background-color: var(--red); + background-image: none; + box-shadow: none; + box-sizing: border-box; + } + + &>span { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + height: 2.5rem; + transition: box-shadow 0.15s ease; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; + + white-space: nowrap; + word-break: break-word; + line-height: 1.5em; + + color: #8898aa; + border: 1px solid #dee2e6; + border-radius: .25rem 0 0 .25rem; + outline: 0; + background-image: none; + box-shadow: none; + box-sizing: border-box; + transition: box-shadow .15s ease; + + } + } + + &>.add-item { + display: inline-flex; + align-items: center; + width: 100%; + min-width: 15.5rem; + flex-wrap: nowrap; + + input { + display: block; + padding: 0.5rem 0.75rem; + box-sizing: border-box; + min-width: 15.5rem; + width: 15.5rem; + transition: box-shadow 0.15s ease; + + white-space: nowrap; + word-break: break-word; + font-size: .875rem; + line-height: 1.5rem; + + color: #8898aa; + border: 1px solid #dee2e6; + border-radius: .25rem 0 0 .25rem; + border-right-width: 0; + outline: 0; + background-image: none; + box-shadow: none; + } + + .cbi-button { + display: flex; + width: auto !important; + padding-left: 0.8rem; + padding-right: 0.8rem; + margin-left: 0; + align-items: center; + justify-content: center; + + font-size: 0.875rem; + line-height: 1.5rem; + + outline: 0; + background-image: none; + background-color: var(--gray); + box-shadow: none; + color: var(--white); + border-color: var(--gray); + border-radius: 0.25rem; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + .cbi-button-add { + width: 2.5rem !important; + padding: 0.5rem 0 !important; + + font-weight: normal; + font-size: 1.2rem; + + color: #fff; + background-color: var(--primary); + border: 1px solid var(--primary); + } + + + &:not([ondrop]) { + &>input { + overflow: hidden; + min-width: 15.5rem; + width: 15.5rem; + white-space: nowrap; + text-overflow: ellipsis; + } + } + } +} + +.cbi-dynlist>.item.drag-over, +.cbi-dynlist>.add-item:has(.drag-over) { + border-top: 1px solid var(--red); + margin-top: -1px; +} + +.cbi-dynlist[name="sshkeys"]>.item { + max-width: none; +} + + +.cbi-dynlist>.cbi-dynlist>.add-item[ondrop]>input { + min-width: 13rem; +} + +.cbi-dynlist, +.cbi-dropdown { + position: relative; + display: inline-flex; + padding-right: 0.25rem; + min-height: 2.1875rem; +} + +.cbi-dropdown { + align-items: center; +} + +.cbi-dropdown.btn>ul:not(.dropdown), +.cbi-dropdown.cbi-button>ul:not(.dropdown) { + margin: 0 0.75rem; +} + +.cbi-dropdown[placeholder*="select"] { + max-width: 25rem; + height: auto; + margin-top: -3px; +} + +.cbi-dropdown>ul { + display: flex; + overflow-x: hidden; + overflow-y: auto; + width: 100%; + padding: 0; + list-style: none; + outline: 0; +} + + +.cbi-dropdown>ul.preview { + display: none; + +} + +.cbi-dropdown.cbi-button-apply, +.cbi-dropdown.cbi-button-action { + padding: 0; +} + +.cbi-button-apply>ul.preview { + display: none; + + li { + color: #fff; + } + +} + +.cbi-button-apply>ul:first-child { + li { + color: #fff; + } + +} + +.cbi-dropdown>.open { + display: flex; + align-items: center; + width: 1rem; + height: 100%; + padding: 0 0.75rem; + cursor: pointer; + user-select: none; + font-size: 0; + color: #8898aa; + background-color: currentColor; + mask-image: var(--dropdown-arrow-icon); + mask-repeat: no-repeat; + mask-size: 100% 1rem; + mask-position: center; + mask-mode: match-source; +} + + +.cbi-dropdown>.more, +.cbi-dropdown>ul>li[placeholder] { + display: none; +} + +.cbi-dropdown>ul>li { + display: none; + overflow: hidden; + align-items: center; + align-self: center; + flex-grow: 1; + flex-shrink: 1; + min-height: 20px; + padding: 0.125rem .25em; + white-space: nowrap; + text-overflow: ellipsis; + +} + +.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-left: thin solid #ccc; +} + +.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 { + margin-right: .25em; + vertical-align: middle; +} + +.cbi-dropdown>ul>li>form>input[type="checkbox"] { + height: auto; + margin: 0; +} + +.cbi-dropdown>ul>li input[type="text"] { + height: 2rem; + min-width: 16rem; + padding: 0 .5rem; +} + +.cbi-dropdown[open]>ul.dropdown { + position: absolute; + z-index: 1100; + display: block; + width: auto; + min-width: 100%; + max-width: none; + max-height: 200px !important; + border: 0 solid #918e8c; + background: #ffffff; + box-shadow: 0 0 4px #918e8c; + border-radius: 0.25rem; + color: var(--main-menu-color); + margin-left: -0 !important; + margin-top: 0.25rem; + left: 0; + + li { + color: #000; + } +} + + +.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 { + display: flex; + align-items: center; + flex-grow: 1; + +} + +.cbi-dropdown[empty]>ul>li, +.cbi-dropdown[optional][open]>ul.dropdown>li[placeholder], +.cbi-dropdown[multiple][open]>ul.dropdown>li>form { + display: block; +} + +.cbi-dropdown[open]>ul.dropdown>li .hide-open { + display: none; +} + +.cbi-dropdown[open]>ul.dropdown>li .hide-close { + display: initial; +} + +.cbi-dropdown[open]>ul.dropdown>li { + border-bottom: thin solid #ccc; + padding: 0.5rem 0.75rem; + cursor: pointer; +} + +.cbi-dropdown[open]>ul.dropdown>li label { + margin-left: 0.5rem; +} + +.cbi-dropdown[open]>ul.dropdown>li[selected] { + background: #e4e9ee; +} + +.cbi-dropdown[open]>ul.dropdown>li.focus { + background: #e4e9ee; + outline: none; +} + +.cbi-dropdown[open]>ul.dropdown>li:last-child { + margin-bottom: 0; + border-bottom: 0; +} + +.cbi-dropdown[open]>ul.dropdown>li[unselectable] { + opacity: .7; +} + +.cbi-dropdown[open]>ul.dropdown>li>input.create-item-input:first-child:last-child { + width: 100%; +} + +.cbi-dropdown[disabled] { + pointer-events: none; + opacity: .6; +} + +.cbi-dropdown .zonebadge { + width: 100%; +} + +.cbi-dropdown[open] .zonebadge { + width: auto; +} + +/* progressbar */ +@keyframes progress-stripes { + 0% { background-position: 1rem 0; } + 100% { background-position: 0 0; } +} + +.cbi-progressbar { + position: relative; + display: flex; + width: 100%; + font-size: 0.75rem; + background-color: #e9ecef; + border-radius: 0.5rem; + height: 1rem; + overflow: hidden; + + &>div { + display: block; + position: absolute; + height: 100%; + background-color: var(--primary); + background-image: linear-gradient( + 45deg, + rgba(255, 255, 255, 0.2) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, 0.2) 50%, + rgba(255, 255, 255, 0.2) 75%, + transparent 75%, + transparent + ); + background-size: 1rem 1rem; + border-radius: 0.5rem; + transition: width 0.3s; + animation: progress-stripes 0.75s linear infinite; + } + + &::after { + content: attr(title); + position: absolute; + font-size: 0.75rem; + color: var(--bs-heading-color); + width: 100%; + height: 100%; + text-align: center; + line-height: 1rem; + z-index: 2; + } +} + +#modal_overlay { + position: fixed; + z-index: 900; + top: 0; + right: 10000px; + bottom: 0; + left: -10000px; + overflow-y: scroll; + transition: opacity .125s ease-in; + opacity: 0; + background: rgba(0, 0, 0, .7); + -webkit-overflow-scrolling: touch; +} + +.modal { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 90%; + min-width: 270px; + max-width: 600px; + min-height: 32px; + margin: 5em auto; + padding: 1rem; + border-radius: 0.25rem !important; + background: #fff; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); + + &>* { + line-height: normal; + flex-basis: 100%; + margin-bottom: .5em; + max-width: 100%; + } + + &>pre, + &>textarea { + font-size: 1rem; + font-size-adjust: .35; + overflow: auto; + ; + padding: 0.5rem; + cursor: auto; + white-space: pre-wrap; + color: var(--dark-primary); + outline: 0; + border-radius: 0.25rem; + border: 1px solid var(--lighter); + transition: all 0.2s ease; + + &:focus { + border: 1px solid var(--primary); + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); + } + } + + &>h4 { + display: block; + flex-grow: 1; + max-width: none; + padding: 1rem; + margin: -1rem -1rem 0 -1rem; + font-size: 1rem; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); + border-radius: 0.25rem 0 0 0.25rem; + } + + h5 { + margin-top: 1rem; + font-weight: 600; + } + + label>input[type="checkbox"] { + top: 0; + } + + ul { + li { + list-style-type: square; + color: #808080; + } + } + + p { + word-break: break-word; + margin-top: 1rem; + } + + .label { + font-size: .6rem; + font-weight: normal; + padding: .1rem .3rem; + padding-bottom: 0; + cursor: default; + border-radius: 0; + } + + .label.warning { + background-color: #f0ad4e !important; + } + + .btn { + padding: 0.45rem 0.8rem; + } + + &.cbi-modal { + max-width: 90%; + max-height: none; + } + + .cbi-map { + margin-bottom: 1rem; + } +} + + +body.modal-overlay-active { + overflow: hidden; + height: 100vh; +} + +body.modal-overlay-active #modal_overlay { + right: 0; + left: 0; + opacity: 1; +} + + +.spinning { + position: relative; + padding-left: 32px !important; +} + +.spinning::before { + position: absolute; + top: 0; + bottom: 0; + + left: .2em; + width: 32px; + content: ""; + background: url(/luci-static/resources/icons/loading.svg) no-repeat center; + background-size: 16px; +} + + + +/* luci */ +.hidden { + display: none; +} + +.left, +.left::before { + text-align: left !important; +} + +.right, +.right::before { + text-align: right !important; +} + +.button-row { + display: flex; + flex-direction: row; + gap: 1rem; + align-items: center; + justify-content: flex-end; + margin-bottom: 0 !important; +} + +.center, +.center::before { + text-align: center !important; +} + +.top { + align-self: flex-start !important; + vertical-align: top !important; +} + +.bottom { + align-self: flex-end !important; + vertical-align: bottom !important; +} + +.inline { + display: inline; +} + +.cbi-page-actions { + padding: 1rem; + justify-content: flex-end; + display: flex; + gap: 1rem; +} + +.cbi-page-actions>form[method="post"] { + display: inline-block; +} + +.th[data-type="button"], +.td[data-type="button"], +.th[data-type="fvalue"], +.td[data-type="fvalue"] { + flex: 1 1 2em; + text-align: center; +} + +.ifacebadge { + display: inline-flex; + align-items: center; + gap: .2rem; + padding: .25rem .8rem; + + background: #eee; + border-radius: 4px; +} + +td>.ifacebadge, +.td>.ifacebadge { + font-size: .875rem; + background-color: #f0f0f0; +} + +.ifacebadge>em, +.ifacebadge>img { + display: inline-block; + margin: 0 0.75rem; +} + +.ifacebadge>img+img { + margin: 0 .2rem 0 0; +} + +.network-status-table { + display: flex; + flex-wrap: wrap; + + .ifacebox { + flex-grow: 1; + border-radius: .25rem; + overflow: hidden; + margin: 1rem; + } + + .ifacebox-body { + display: flex; + flex-direction: column; + height: 100%; + gap: 0.5em; + + >span { + flex: 10 10 auto; + } + + >div { + display: flex; + flex-wrap: wrap; + gap: .5rem; + height: 100%; + } + + .ifacebadge { + align-items: center; + flex: 1 1 auto; + min-width: 220px; + padding: .5em; + background-color: #fff; + + >span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } +} + +/* textarea */ +.cbi-input-textarea { + font-family: inherit; + width: 100%; + font-size: 0.875rem; + min-height: 14rem; + padding: .8rem; + color: #8898aa; + border-radius: 0.25rem; + border: 1px solid #dee2e6; + min-width: 16rem; +} + +#content_syslog { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); +} + +#syslog { + font-size: small; + font-family: 'Red Hat Display'; + line-height: 1.25; + overflow-y: hidden; + width: 100%; + min-height: 15rem; + padding: 1rem; + resize: none; + color: #242424; + border: 0; + border-radius: 0.25rem; + background-color: #ffffff; + +} + +#syslog:focus { + outline: 0; +} + +#cbi-ddns #syslog { + overflow: auto; +} + +/* config changes */ +.uci-change-list { + font-family: inherit; + overflow: scroll; + width: 100%; + display: flex; + flex-direction: column; + flex-wrap: wrap; +} + +.uci-change-list ins, +.uci-change-legend-label ins { + display: block; + padding: 2px; + text-decoration: none; + border: thin solid #0f0; + background-color: #cfc; +} + +.uci-change-list del, +.uci-change-legend-label del { + font-style: normal; + display: block; + padding: 2px; + text-decoration: none; + border: thin solid #f00; + background-color: #fcc; +} + +.uci-change-list var, +.uci-change-legend-label var { + font-style: normal; + display: block; + padding: 2px; + text-decoration: none; + border: thin solid #ccc; + background-color: #eee; +} + +.uci-change-list var ins, +.uci-change-list var del { + font-style: normal; + padding: 0; + white-space: pre; + border: 0; +} + +.uci-change-legend { + padding: 5px; +} + +.uci-change-legend-label { + float: left; + width: 150px; +} + +.uci-change-legend-label>ins, +.uci-change-legend-label>del, +.uci-change-legend-label>var { + display: block; + float: left; + width: 10px; + height: 10px; + margin-right: 4px; +} + +.uci-change-legend-label var ins, +.uci-change-legend-label var del { + line-height: .4; + border: 0; +} + +.uci-change-list var, +.uci-change-list del, +.uci-change-list ins { + padding: .5rem; +} + +.uci-dialog { + .cbi-section { + padding: 0.5rem; + + .uci-change-legend { + line-height: 15px; + padding: 10px 20px 0 20px; + + .uci-change-legend-label { + padding: 0; + margin: 0; + position: relative; + float: none; + display: inline-block; + width: 25%; + + >ins, + >del { + width: 14px; + height: 14px; + } + + >var { + position: relative; + width: 14px; + height: 14px; + + ins, + del { + position: absolute; + left: 2px; + top: 2px; + right: 2px; + bottom: 2px; + } + } + } + } + + .uci-change-list { + overflow: auto; + } + + .uci-change-list+.right { + .btn { + color: #333; + } + + .cbi-dropdown ul:not(.dropdown) { + li { + color: #fff; + } + } + + .cbi-button { + padding: .45rem .8rem; + } + } + } +} + +/* other fix */ +#iwsvg, +#iwsvg2, +#bwsvg { + border: thin solid #d4d4d4 !important; +} + +#iwsvg { + border-top: 0 !important; +} + +.ifacebox { + line-height: 1.25; + display: inline-flex; + overflow: hidden; + flex-direction: column; + border-radius: 4px; + min-width: 100px; + background-color: #f9f9f9; + // border-bottom: thin solid #ccc; + // box-shadow: inset 0 1px 0 rgba(255, 255, 255, .4), 0 1px 2px rgba(0, 0, 0, .2); +} + +.ifacebox-head { + padding: .25em; + background: #eee; +} + +.ifacebox-head.active { + background: #5e72e4; + background: var(--primary); + + * { + color: #fff; + color: var(--white); + } + +} + +.ifacebox-body { + padding: 0.875rem 1rem; + line-height: 1.6em; +} + +.cbi-image-button { + margin-left: .5rem; +} + +.zonebadge { + display: inline-block; + padding: .2rem .5rem; + border-radius: 4px; +} + +.zonebadge .ifacebadge { + margin: .1rem .2rem; + padding: .2rem .3rem; + border: thin solid #6c6c6c; +} + +.zonebadge>input[type="text"] { + min-width: 10rem; + margin-top: .3rem; + padding: .16rem 1rem; +} + +.zonebadge>em, +.zonebadge>strong { + display: inline-block; + margin: 0 .2rem; +} + +.cbi-value-field .cbi-input-checkbox, +.cbi-value-field .cbi-input-radio { + margin-top: .1rem; +} + +.cbi-value-field>ul>li { + display: flex; +} + +.cbi-value-field>ul>li>label { + margin-top: .5rem; +} + +.cbi-value-field>ul>li .ifacebadge { + margin-top: -.5rem; + margin-left: .4rem; + background-color: #eee; +} + +.cbi-section-table-row>.cbi-value-field .cbi-dropdown { + min-width: 3rem; +} + +.cbi-section-create { + display: inline-flex; + align-items: center; + padding: .875rem 1rem; + + + &>div:first-child { + &>input { + width: 100%; + border-right: none; + border-bottom-right-radius: 0; + border-top-right-radius: 0; + } + } + + &>div:first-child+button { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } +} + + +.cbi-section-remove { + padding: .5rem 1rem; +} + +div.cbi-value var, +td.cbi-value-field var, +.td.cbi-value-field var { + font-style: italic; + color: #0069d6; +} + +.cbi-optionals { + padding: 1rem 1rem 0 1rem; + border-top: thin solid #ccc; +} + +.cbi-dropdown-container { + position: relative; +} + +.cbi-tooltip-container, +span[data-tooltip], +span[data-tooltip] .label { + cursor: help !important; +} + +.cbi-tooltip { + position: absolute; + z-index: 1000; + left: -10000px; + box-shadow: 0 3px 9px rgba(50, 50, 9, 0), 3px 4px 8px rgba(94, 114, 228, 0.1); + border-radius: 0.25rem; + background: #fff; + white-space: pre; + padding: 0.5rem; + opacity: 0; + transition: opacity .25s ease-in; + transform: translate(-50%, 10%); +} + +.cbi-tooltip-container:hover .cbi-tooltip:not(:empty) { + left: auto; + transition: opacity .25s ease-in; + opacity: 1; +} + +.zonebadge .cbi-tooltip { + margin: -1.5rem 0 0 -.5rem; + padding: .25rem; + background: inherit; +} + +.zonebadge-empty { + color: #404040; + background: repeating-linear-gradient(45deg, rgba(204, 204, 204, .5), rgba(204, 204, 204, .5) 5px, rgba(255, 255, 255, .5) 5px, rgba(255, 255, 255, .5) 10px); +} + +.zone-forwards { + display: flex; + min-width: 10rem; +} + +.zone-forwards>* { + flex: 1 1 45%; +} + +.zone-forwards>span { + flex-basis: 10%; + padding: 0 .25rem; + text-align: center; +} + +.zone-forwards .zone-src, +.zone-forwards .zone-dest { + display: flex; + flex-direction: column; +} + +.label { + font-size: .875rem; + font-weight: bold; + padding: .3rem .8rem; + white-space: nowrap; + text-decoration: none; + text-transform: uppercase; + color: #fff !important; + border-radius: 3px; + background-color: #bfbfbf; + text-shadow: none; +} + +label>input[type="checkbox"], +label>input[type="radio"] { + position: relative; + top: .4rem; + right: .2rem; + margin: 0; + vertical-align: bottom; +} + +label[data-index][data-depends] { + padding-right: 2em; +} + +.showSide { + display: none; +} + +.darkMask { + position: fixed; + z-index: 99; + display: none; + width: 100%; + height: 100%; + content: ""; + top: 0; + background-color: rgba(0, 0, 0, .56); + transition: all 0.2s; + + &.active { + display: block; + } +} + +/* diagnostics */ +#diag-rc-output>pre, +#command-rc-output>pre, +[data-page="admin-services-wol"] .notice code { + font-size: 1.2rem; + font-size-adjust: .35; + line-height: normal; + display: block; + overflow-y: hidden; + width: 100%; + padding: 8.5px; + white-space: pre; + color: #eee; + background-color: #101010; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .16), 0 0 2px 0 rgba(0, 0, 0, .12); +} + + + +input[name="ping"], +input[name="traceroute"], +input[name="nslookup"] { + width: 80%; +} + +.controls { + gap: 0.5rem; + margin: 0 1.25rem !important; +} + +.controls>div { + gap: 0.5rem; +} + +.controls>*>.btn:not([aria-label$="page"]) { + flex-grow: initial !important; + margin-top: .25rem; +} + +.controls>#pager>.btn[aria-label$="page"] { + font-size: 1.4rem; + font-weight: bold; +} + +.controls>*>label { + margin-bottom: .2rem; +} + +.td.version, +.td.size { + white-space: normal !important; + word-break: break-word; +} + +.cbi-tabmenu+.cbi-section { + margin-top: 0; +} + + +.cbi-tab-disabled[data-errors]::after { + position: absolute; + top: -0.25rem; + right: -0.25rem; + content: attr(data-errors); + background-color: var(--red); + color: #fff; + width: 0.875rem; + height: 0.875rem; + border-radius: 0.875rem; + text-align: center; + display: inline-flex; + flex-direction: column; + justify-content: center; + font-size: .75em; +} + + + + +/* custom commands */ +.commands { + display: grid !important; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 0.75rem; +} + +.commandbox { + padding: .5rem 1rem; + gap: 0.5rem; + border-bottom: thin solid #ccc; + border-radius: .25rem; + background: #eee; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .2), 0 1px 2px rgba(0, 0, 0, .05); + + h3 { + line-height: normal !important; + overflow: hidden; + margin: 6px 0 !important; + white-space: nowrap; + text-overflow: ellipsis; + } + + div { + left: auto !important; + } + + code { + overflow: hidden; + max-width: fit-content; + padding: 2px 3px; + white-space: nowrap; + text-overflow: ellipsis; + + &:hover { + overflow-y: auto; + max-height: 50px; + white-space: normal; + } + } + + p:first-of-type { + margin-top: -6px; + } + + p:nth-of-type(2) { + margin-top: 2px; + } +} + + + +#command-rc-output .alert-message { + line-height: 1.42857143; + position: absolute; + top: 40px; + right: 32px; + max-width: 40%; + margin: 0; + animation: anim-fade-in 1.5s forwards; + word-break: break-word; + opacity: 0; +} + +@keyframes anim-fade-in { + 100% { + opacity: 1; + } +} + +/* other fix */ + + +.fb-container .cbi-button { + height: auto !important; +} + +#cbi-usb_printer-printer em { + display: block; + padding: 1rem; + text-align: center; +} + +pre.command-output { + padding: 1.5rem; +} + + +/* page fix */ +@import url("page-fix.less"); + +/* responsive */ +@import url("responsive.less"); + + diff --git a/luci-theme-noobwrt/less/dark.less b/luci-theme-noobwrt/less/dark.less new file mode 100644 index 000000000..c89b46b7d --- /dev/null +++ b/luci-theme-noobwrt/less/dark.less @@ -0,0 +1,1266 @@ +// out: ../htdocs/luci-static/noobwrt/css/dark.css, compress: true , sourceMap: false +/** + * NoobWrt is a clean HTML5 theme for LuCI. It is based on luci-theme-material and NoobWrt Template + * + * luci-theme-noobwrt + * Copyright 2025 Jerryk + * + * Have a bug? Please create an issue here on GitHub! + * https://github.com/jerrykuku/luci-theme-noobwrt/issues + * + * luci-theme-bootstrap: + * Copyright 2008 Steven Barth + * Copyright 2008 Jo-Philipp Wich + * Copyright 2012 David Menting + * + * MUI: + * https://github.com/muicss/mui + * + * luci-theme-material: + * https://github.com/LuttyYang/luci-theme-material/ + * + * Agron Theme + * https://demos.creative-tim.com/argon-dashboard/index.html + * + * Login background + * https://unsplash.com/ + * + * Licensed to the public under the Apache License 2.0 + */ + + +body { + background: #1e1e1e; + color: #cccccc; +} + +.spinning::before { + filter: invert(1); +} + +.login-page .login-container { + + .login-form { + .login-card { + background-color: rgba(18, 18, 28, 0.92); + -webkit-backdrop-filter: blur(var(--blur-radius-dark)) brightness(0.9); + backdrop-filter: blur(var(--blur-radius-dark)) brightness(0.9); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45); + + .login-hero { + background: linear-gradient(135deg, rgba(33, 33, 44, 0.95), rgba(26, 27, 40, 0.95)); + + .hero-particles { + span { + background: rgba(124, 118, 255, 0.4); + box-shadow: 0 0 18px rgba(124, 118, 255, 0.45); + } + } + + .hero-icon { + background: rgba(124, 118, 255, 0.08); + border: 1px solid rgba(124, 118, 255, 0.28); + box-shadow: 0 0 28px rgba(124, 118, 255, 0.35), inset 0 0 18px rgba(124, 118, 255, 0.12); + } + + } + } + + .brand { + color: #adb5bd; + } + + .brand-text-only { + color: #f3f4f6; + } + + .login-subtitle { + color: #b7bdcc; + font-weight: 500; + } + + .form-login { + .errorbox { + color: #fecaca; + background: rgba(220, 38, 38, 0.15); + border: 1px solid rgba(248, 113, 113, 0.3); + } + + .input-group { + &::before { + color: #a1a8b8; + } + + input { + background-color: rgba(30, 30, 40, 0.9) !important; + color: #e5e7eb; + border: 1px solid rgba(255, 255, 255, 0.08) !important; + border-radius: 999px !important; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.35), 0 10px 20px rgba(0, 0, 0, 0.35); + + &:focus { + border-color: rgba(99, 102, 241, 0.65) !important; + background-color: rgba(36, 36, 48, 0.95) !important; + box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.25); + } + + &::placeholder { + color: #8f96a8; + } + } + + } + + .cbi-button-apply { + background: linear-gradient(135deg, #7c76ff, #5b8dff) !important; + + &:hover, + &:focus { + opacity: 0.95; + } + } + } + } + +} + +header::after { + background-color: #1e1e1e !important; +} + + +.main { + .main-left { + + background-color: #333333 !important; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .15); + + .sidenav-header .brand { + color: #cccccc; + } + + .nav { + .slide { + .slide-menu { + .active { + a { + color: #cccccc; + + &::after { + background-color: #cccccc !important; + } + } + + &::after { + background-color: var(--dark-primary) !important; + } + } + + li { + a { + color: #cccccc; + } + + a:hover { + background: none !important; + } + + &::after { + background-color: var(--dark-primary) !important; + } + } + } + + .menu.active { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + color: #ffffff !important; + + a::after { + background-color: #ffffff !important; + } + } + } + + li { + a { + color: #cccccc !important; + } + + a:hover { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + color: #ffffff !important; + } + + } + } + + + &::-webkit-scrollbar-thumb { + background-color: #252526 !important; + } + + &::-webkit-scrollbar-track { + background-color: #333; + } + } + + .main-right { + background-color: #1e1e1e; + } +} + +h2 { + color: #ccc; + background: #333333; +} + +h3 { + color: #ccc; + border-bottom: 0; + background: #333333; +} + +h4 { + color: #8C6900; +} + +abbr { + color: var(--dark-primary) !important; +} + +a:link, a:visited, a:active { + color: #a5b2ff; +} + + +input:-webkit-autofill { + background-color: #3c3c3c !important; +} + +#channel_graph { + background-color: transparent !important; +} + +.cbi-value-field .cbi-input-apply, +.cbi-button-apply, +.cbi-button-edit { + color: #fff !important; + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + border-color: #483d8b !important; + border-color: var(--dark-primary) !important; +} + + +.cbi-section em { + color: #ccc; +} + +header.bg-primary { + background-color: #1e1e1e !important; +} + +.cbi-map-descr { + color: #ccc; +} + +.cbi-section { + background: none; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .35) +} + +.panel-title { + color: #ccc; + background-color: #333333; +} + +div > table > tbody > tr:nth-of-type(2n), +div > .table > .tr:nth-of-type(2n) { + background-color: #252526; +} + +table > tbody > tr > td, +table > tfoot > tr > td, +table > thead > tr > td { + color: #cccccc; +} + +fieldset > table > tbody > tr:nth-of-type(2n) { + background-color: #252526; +} + +table > tbody > tr > td, +table > tfoot > tr > td, +table > thead > tr > td { + border-top: 1px solid #252526; +} + +#swaptotal > div > div, +#swapfree > div > div, +#memfree > div > div, +#membuff > div > div, +#conns > div > div, +#memtotal > div > div { + background-color: var(--dark-primary) !important; +} + +#swaptotal > div > div > div > small, +#swapfree > div > div > div > small, +#memfree > div > div > div > small, +#membuff > div > div > div > small, +#conns > div > div > div > small, +#memtotal > div > div > div > small { + color: #ccc !important; +} + +.node-system-packages > .main .cbi-section-node:first-child .cbi-value-last { + line-height: 1.8em; + + div[style="margin:3px 0; width:300px; height:10px; border:1px solid #000000; background-color:#80C080"] { + border: 1px solid #999999 !important; + background-color: transparent !important; + + div { + background-color: #ba8b00 !important; + } + } + +} + +table > tbody > tr > th, +table > tfoot > tr > th, +table > thead > tr > th { + background-color: #252526; + border-bottom: black 1px solid !important; +} + +.tr[data-title]::before, +.tr.cbi-section-table-titles.named::before { + background-color: #252526; +} + +tr > td, +tr > th, +.tr > .td, +.tr > .th, +.cbi-section-table-row::before, +#cbi-wireless > #wifi_assoclist_table > .tr:nth-child(2) { + border-top: 0; +} + +.cbi-rowstyle-2 { + background-color: #1e1e1e; +} + +.cbi-rowstyle-1 { + background-color: #252526; +} + +.cbi-rowstyle-2 .cbi-button-up, +.cbi-rowstyle-2 .cbi-button-down, +body:not(.Interfaces) .cbi-rowstyle-2:first-child { + background-color: rgb(102, 102, 102) !important; +} + +.cbi-section > h3:first-child, +.panel-title, +h3 { + color: #ccc; +} + +h4 { + background-color: #1e1e1f; +} + +.cbi-progressbar { + background: #282a2c; + + div { + background-color: var(--dark-primary) !important; + } +} + +.cbi-section-table .cbi-section-table-titles .cbi-section-table-cell { + background-color: #1e1e1f; +} + +.cbi-button { + color: #ffffff !important; + background-color: #483d8b; + background-color: var(--dark-primary); +} + +.cbi-section-node { + background: none; +} + +abbr { + color: #5e72e4; +} + +div > table > tbody > tr:nth-of-type(2n), +div > .table > .tbody > .tr:nth-of-type(2n) { + background-color: #252526; +} + +#content_syslog { + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .35) +} + +#syslog { + color: #ccc; + background-color: #1e1e1e; +} + +#iwsvg, +#iwsvg2, +#bwsvg { + overflow: hidden; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .35); + background-color: #1e1e1e !important; +} + +.tabs { + background-color: #252526; + + li[class~="active"], + li:hover { + cursor: pointer; + color: #ccc !important; + + a { + color: #ccc !important; + } + } +} + +.tabs > li[class~="active"] > a { + color: #ccc; +} + +.tabs > li[class~="active"], +.tabs > li:hover { + border-bottom: 0.18751rem solid #483d8b; + border-bottom: 0.18751rem solid var(--dark-primary); + color: var(--dark-primary) !important; + background-color: #181819; +} + +.cbi-tabmenu > li > a, +.tabs > li > a { + color: #ccc; + + &:hover { + color: #ccc; + } +} + +.cbi-tabmenu > li { + background: #2d2d2d; +} + +.cbi-tabmenu li[class~="cbi-tab"] a { + color: #ccc; +} + +.cbi-tabmenu > li:hover { + color: #ccc; + background: #2d2d2d; + border-bottom: 0.18751rem solid var(--dark-primary) !important; +} + +.cbi-tabmenu > li[class~="cbi-tab"] { + background-color: #181819; + border-bottom: 0.18751rem solid var(--dark-primary) !important; +} + +.cbi-tabcontainer > .cbi-value:nth-of-type(2n) { + background-color: #252526; +} + +.cbi-value-title { + color: #ccc; +} + +select, +input { + color: #ccc; + background-color: transparent !important; + border: 1px solid #252526; + box-shadow: none; +} + + +select:not([multiple="multiple"]):focus, +input:focus { + border-color: #483d8b !important; + border-color: var(--dark-primary) !important; + outline: 0; + +} + +select:not([multiple="multiple"]):focus, +input:not(.cbi-button):focus, +.cbi-dropdown:focus { + border-color: #5e72e4; + border-color: var(--dark-primary) !important; +} + +select { + background-color: #1e1e1e !important; +} + +#cbi-dropbear h2, +#cbi-dropbear .cbi-map-descr, +#cbi-dropbear .cbi-map-descr abbr, +#cbi-rc h2, +#cbi-rc .cbi-map-descr, +#cbi-distfeedconf h2, +#cbi-distfeedconf .cbi-map-descr, +#cbi-customfeedconf h2, +#cbi-customfeedconf .cbi-map-descr, +#cbi-download h2, +#cbi-filelist h2 { + color: #ccc !important; +} + +.cbi-value-field > ul > li .ifacebadge { + background-color: #3c3c3c; +} + +.cbi-section-descr { + color: #ccc; +} + +.cbi-input-textarea { + background-color: #1e1e1e; + color: #ccc; + border: 1px solid #3c3c3c !important; +} + +textarea { + &:focus-visible { + border: 1px solid var(--dark-primary); + } +} + + +.cbi-section-remove:nth-of-type(2n), +.cbi-section-node:nth-of-type(2n) { + background-color: #1e1e1e; +} + +.cbi-section[id] .cbi-section-remove:nth-of-type(4n+3), +.cbi-section[id] .cbi-section-node:nth-of-type(4n+4) { + background-color: #1e1e1e; +} + +.node-system-packages > .main table tr td:nth-last-child(1) { + color: #ccc; + +} + +.cbi-tooltip { + background-color: var(--primary); + color: #fff; +} + +.ifacebox { + background-color: transparent !important; + border: 1px solid #1e1e1e; +} + +.ifacebox-head { + color: #ccc; + background: #666; +} + +.ifacebox-body { + background-color: #333; +} + +.zonebadge strong { + color: #333; +} + +.zonebadge > .ifacebadge { + background-color: #3c3c3c; +} + +div.cbi-value var, +td.cbi-value-field var { + color: #5e72e4; +} + +#diag-rc-output > pre { + color: #ccc; + background-color: #1e1e1e; +} + +.node-services-vssr .block { + background-color: #3c3c3c !important; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .35); +} + +.node-services-vssr .block h4 { + color: #ccc !important; +} + +.node-services-vssr .status-bar { + color: #ccc; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .35); + background-color: #1e1e1e; +} + +.node-services-vssr .cbi-section-table-row { + color: #ccc; + background-color: #3c3c3c !important; + box-shadow: 0 0 5px 0 rgba(0, 0, 0, .35) +} + +.node-services-vssr .cbi-section-table-row.fast { + background: #483d8b !important; + background: var(--dark-primary) !important; + color: #fff; +} + +.node-services-vssr .ssr-button { + color: #ccc; + +} + +.node-services-vssr .incon:nth-child(2) { + border-right: #1e1e1e 1px solid; +} + +#xhr_poll_status > .label.success { + color: #ccc !important; + + background-color: darkolivegreen !important; +} + +.notice { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; +} + +.cbi-section.fade-in .cbi-title>div:last-child span { + background: none!important; + color: #ced4da; +} + +.cbi-input-find, +.cbi-input-save, +.cbi-button-add, +.cbi-button-save, +.cbi-button-find, +.cbi-input-reload, +.cbi-button-reload { + color: #fff !important; + background: darkolivegreen !important; + border-color: darkolivegreen !important; +} + +.cbi-button-reset, +.cbi-input-remove { + color: #fff !important; + background-color: darkorange !important; + border-color: darkorange !important; +} + +.cbi-page-actions .cbi-button-apply, +.cbi-section-actions .cbi-button-edit, +.cbi-button-edit.important, +.cbi-button-apply.important, +.cbi-button-reload.important, +.cbi-button-action.important { + border: 1px #483d8b solid !important; + border: 1px var(--dark-primary) solid !important; +} + +.btn[value="Dismiss"], +.cbi-button[value="Terminate"], +.cbi-button[value="Reset"], +.cbi-button[value="Disabled"], +.cbi-button[onclick^="iface_reconnect"], +.cbi-button[onclick="handleReset(event)"], +.cbi-button-neutral[value="Disable"] { + color: #fff; + border: thin solid darkorange !important; + background-color: darkorange !important; +} + +fieldset[id^="cbi-apply-"] { + background-color: #333333; +} + +#detail-bubble > div { + border: 1px solid #ccc; + background: #252525; +} + +.ifacebox-head.active { + background-color: var(--dark-primary) !important; +} + +header .fill .status span[data-style="active"] { + color: #ccc !important; + background-color: darkolivegreen !important; +} + +#cbi-wireless .td, +#cbi-network .tr:first-child > .td, +.table[width="100%"] > .tr:first-child > .td, +[data-page="admin-network-diagnostics"] .tr > .td, +.tr.table-titles > .th, +.tr.cbi-section-table-titles > .th { + background-color: #252526; + border-bottom: black 1px solid !important; + +} + +.network-status-table .ifacebox-body .ifacebadge { + background-color: #252526; + border-bottom: 0; + box-shadow: none; +} + +td > .ifacebadge, +.td > .ifacebadge { + color: #fff; + background-color: #483d8b; + background-color: var(--dark-primary); + border: 0; +} + +.alert, .alert-message { + background-color: #333; +} + +.alert-message.warning { + background-color: #986400 !important; +} + +.alert.error, .alert-message.error { + background-color: #784f00; +} + +.alert h4, .alert-message h4 { + background-color: #784f00; + color: #ffbf00; +} + +.alert-message [class="btn"] { + background-color: #777777; + color: #ccc; +} + +.uci-dialog { + .cbi-section { + + .uci-change-legend { + + .uci-change-legend-label { + + > ins { + border-color: #00ad00; + background-color: #688668; + } + + > del { + border-color: #c60000; + background-color: #896565; + } + + > var { + background-color: #333; + border-color: #666; + + ins { + background-color: #688668; + } + + del { + background-color: #896565; + } + } + } + } + + .uci-change-list { + > var { + background-color: #333; + border-color: #666; + + del { + background-color: #896565; + } + + ins { + background-color: #688668; + } + } + + > ins { + border-color: #00ad00; + background-color: #688668; + } + + > del { + border-color: #c60000; + background-color: #896565; + } + } + + .uci-change-list + .right { + .btn { + color: #ccc; + } + + .cbi-button { + border: 1px solid #3c3c3c !important; + } + } + } +} + +.btn.danger, +.cbi-section-remove > .cbi-button, +.cbi-button-remove, +.cbi-button-reset, +.cbi-button-negative, +.cbi-button[value="Stop"], +.cbi-button[value="Kill"], +.cbi-button[onclick="reboot(this)"], +.cbi-button-neutral[value="Restart"] { + border: thin solid darkorange !important; + background-color: darkorange !important; +} + +.cbi-section, +.cbi-section-error, +#iptables, +.Firewall form, +#cbi-network > .cbi-section-node, +#cbi-wireless > .cbi-section-node, +#cbi-wireless > #wifi_assoclist_table, +[data-tab-title], +[data-page^="admin-system-admin"]:not(.node-main-login) .cbi-map:not(#cbi-dropbear), +[data-page="admin-system-opkg"] #maincontent > .container { + background: #1e1e1e !important; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, 0.35); +} + +div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] { + background: transparent !important; +} + +[data-page="admin-system-admin"] .cbi-map h2, +[data-page="admin-system-admin-password"] .cbi-map h2, +[data-page="admin-system-admin"] .cbi-map .cbi-map-descr, +[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr { + color: #ccc; +} + +[data-page="admin-system-flash"] { + .modal { + label > input[type="checkbox"] { + vertical-align: text-top; + top: auto; + border-color: #fff !important; + } + + .btn { + white-space: normal !important; + background-color: darkseagreen; + } + + .alert-message { + background-color: transparent !important; + } + + .danger { + border: thin solid darkorange !important; + background-color: darkorange !important; + } + } +} + +.cbi-value input[type="password"] + .cbi-button-neutral { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + border-color: var(--dark-primary) !important; +} + +.btn, +button, +select, +input, +.cbi-dropdown { + border: 1px solid #3c3c3c !important; +} + + +.cbi-dropdown .preview { + color: #ccc; +} + +.cbi-section-table-row { + background-color: #1e1e1e !important; +} + +.modal { + background-color: #1e1e1e; +} + +.cbi-button-positive { + color: #fff !important; + background-color: darkolivegreen !important; +} + +[data-page="admin-system-flash"] legend { + color: #ccc; +} + +.logout:before { + color: #adb5bd !important; +} + +.cbi-dropdown[open] { + border-color: #483d8b !important; + border-color: var(--dark-primary) !important; +} + +.cbi-dropdown[open] > ul.dropdown { + background: #252526 !important; + color: #ffffff !important; + box-shadow: none; + border: 1px solid #3c3c3c !important; + + +} + +.cbi-dropdown[open] > ul.dropdown li { + color: #ffffff; + border-bottom: 1px solid #3c3c3c !important; +} + +.cbi-dropdown[open] > ul.dropdown > li[selected] { + background-color: #483d8b !important; + background-color: var(--dark-primary) !important; + border-bottom: 1px solid #3c3c3c !important; +} + +.cbi-dropdown[open] > ul.dropdown > li.focus { + background: #483d8b; + background: var(--dark-primary); + outline: none; +} + +.ifacebadge { + background-color: #333333; +} + +.cbi-dynlist > .item > span { + border: 1px solid #3c3c3c !important; +} + +.cbi-page-actions .cbi-button-apply, +.cbi-section-actions .cbi-button-edit, +.cbi-button-edit, +.cbi-button-apply, +.cbi-button-reload, +.cbi-button-action, +.cbi-button[value="Submit"], +.cbi-button[value="Upload"], +.cbi-button[value$="Apply"], +.cbi-button[onclick="addKey(event)"] { + background: #483d8b !important; + background: var(--dark-primary) !important; + border-color: var(--dark-primary) !important; +} + +.btn.primary, +.cbi-page-actions .cbi-button-save, +.cbi-page-actions .cbi-button-apply + .cbi-button-save, +.cbi-button-add, +.cbi-button-save, +.cbi-button-positive, +.cbi-button-link, +.cbi-button[value="Enable"], +.cbi-button[value="Scan"], +.cbi-button[value^="Back"], +.cbi-button-neutral[onclick="handleConfig(event)"] { + background: #483d8b; + background: var(--dark-primary); +} + +[data-page="admin-system-opkg"] h2 { + color: #ccc !important; +} + +[data-page="admin-system-startup"] textarea { + color: #ccc; + background-color: transparent; + + &:focus-visible { + border: 1px solid var(--dark-primary); + } +} + +[data-page="admin-network-firewall-custom"] #view p textarea, +[data-page="admin-status-routes"] #view p textarea, +[data-page="admin-system-crontab"] #view p textarea { + color: #ccc; + background-color: transparent; +} + +#view > .spinning { + background: #333333 !important; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03) !important; +} + +@media screen and (min-width: 600px) { + + ::-webkit-scrollbar-thumb { + background: var(--dark-primary); + } + + ::-webkit-scrollbar-thumb:hover { + background: var(--dark-primary); + } + + ::-webkit-scrollbar-thumb:active { + background: var(--dark-primary); + } +} + + +@media screen and (max-width: 480px) { + .node-status-iptables > .main div > .cbi-map > form { + background-color: #1e1e1e; + box-shadow: 0 0 0.5rem 0 rgba(0, 0, 0, .35); + } + + +} + +[data-page=""], +[data-page="admin-dashboard"] { + .main-right > #maincontent { + .Dashboard { + color: #ccc !important; + + h3 { + color: #ccc; + } + + hr { + border-top: 1px solid rgba(255, 255, 255, 1); + } + + .dashboard-bg { + background-color: #333333; + } + + img.svgmonochrome { + filter: invert(90%); + } + + tr { + border-top: thin solid #4d4d4d; + + &:last-child { + border-bottom: thin solid #4d4d4d; + } + } + } + } +} + +[data-page="admin-system-fileassistant"] { + .fb-container .panel-container { + border-bottom-color: #3c3c3c !important; + } + + .fb-container td[class$="-icon"]::before { + filter: invert(0.7); + } + + div#list-content { + table.cbi-section-table { + tbody { + tr:nth-child(1) { + td.parent-icon { + background: #1e1e1e !important; + } + } + } + } + } +} + +[data-page^="admin-services-openclash"] { + .oc { + --bg-light: #1e1e1e !important; + --bg-gray: #1e1e1e !important; + + .developer-container { + background: transparent !important; + } + } + + .config-upload-content{ + background: #1e1e1e !important; + } + + .cbi-tabmenu::-webkit-scrollbar-thumb { + background-color: #5b5b5b; + } + + .cbi-tabmenu::-webkit-scrollbar-track { + background-color: rgba(60, 60, 60, 0.75); + } + + #tab { + border-color: #3c3c3c; + } + + #diag-rc-output > pre, + #dns-rc-output > pre { + color: #ccc; + border: 1px solid #3c3c3c !important; + background-color: #1e1e1e; + } + + img[src$="arrow-clockwise-light.svg"], + img[src$="wrench-light.svg"], + img[src$="eye-light.svg"] { + filter: invert(0.9); + } +} + +[data-page="admin-network-diagnostics"] { + .cbi-section { + background: #252526 !important; + } + + textarea { + background: transparent; + border-radius: 0.25rem; + color: #ccc; + border: 1px solid #3c3c3c !important; + } + + .tr > .td { + background-color: #252526 !important; + } +} + +[data-page="admin-network-network"] { + .ifacebox-head[style*="--zone-color-rgb: 144, 240, 144"] { + background-color: rgb(73, 126, 73) !important; + color: #ccc !important; + } + + .ifacebox-head[style*="--zone-color-rgb: 240, 144, 144;"] { + background-color: rgb(156, 79, 79) !important; + color: #ccc !important; + } + + .ifacebox-head[style*="--zone-color-rgb: 238, 238, 238;"] { + background-color: #666 !important; + } + + #modal_overlay > .modal.cbi-modal > div > p > textarea { + border: 1px solid #3c3c3c !important; + background-color: transparent !important; + } + + #modal_overlay div[data-tab="peers"] div[data-name="_peers"] .ifacebadge code { + color: #fff !important; + background-color: transparent !important; + } + + #modal_overlay > .modal.cbi-modal > div > p > textarea { + color: #ccc!important; + } +} + +[data-page="admin-network-firewall-rules"] { + #cbi-firewall-rule { + .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] { + --zone-color-rgb: 156, 79, 79 !important; + + strong { + color: #ccc !important; + } + } + + .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] { + --zone-color-rgb: 112, 112, 112 !important; + + em { + color: #ccc !important; + } + } + + > table > .cbi-section-table-row[data-title]::before { + background-color: #252526 !important; + } + } +} + +[data-page="admin-network-network"], +[data-page="admin-network-firewall-rules"] { + .cbi-dropdown { + .zonebadge[style*="--zone-color-rgb:240, 144, 144;"] { + --zone-color-rgb: 156, 79, 79 !important; + color: #ccc !important; + + strong { + color: #ccc !important; + } + } + + .zonebadge[style*="--zone-color-rgb:144, 240, 144;"] { + --zone-color-rgb: 73, 126, 73 !important; + color: #ccc !important; + + strong { + color: #ccc !important; + } + } + + .zonebadge[style*="--zone-color-rgb:238, 238, 238;"] { + --zone-color-rgb: 112, 112, 112 !important; + + strong { + color: #ccc !important; + + + span { + color: #ccc !important; + } + } + } + } +} + +[data-page="admin-system-commands"], +[data-page="admin-system-commands-dashboard"] { + .commandbox { + border-bottom: thin solid #333; + background: #252526; + box-shadow: inset 0 1px 0 rgba(0,0,0,0.2),0 1px 2px rgba(255,255,255,0.05); + } +} + +.btn { + background-color: rgb(112, 112, 112); + color: #fff; +} + + diff --git a/luci-theme-noobwrt/less/fonts.less b/luci-theme-noobwrt/less/fonts.less new file mode 100644 index 000000000..f38fff8f8 --- /dev/null +++ b/luci-theme-noobwrt/less/fonts.less @@ -0,0 +1,46 @@ +// out: false + +// ========================================================================== +// CSS Custom Properties for Font Management +// ========================================================================== + +@font-face { + font-family: 'Red Hat Display'; + src: url('../fonts/RedHatDisplay-Variable.woff2') format('woff2 supports variations'), + url('../fonts/RedHatDisplay-Variable.woff2') format('woff2'); + font-weight: 300 900; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Red Hat Display'; + src: url('../fonts/RedHatDisplay-Italic-Variable.woff2') format('woff2 supports variations'), + url('../fonts/RedHatDisplay-Italic-Variable.woff2') format('woff2'); + font-weight: 300 900; + font-style: italic; + font-display: swap; +} + +/* Logo Font */ +@font-face { + font-family: 'TypoGraphica'; + src: local('TypoGraphica'), + url('../fonts/TypoGraphica.woff2') format('woff2'), + url('../fonts/TypoGraphica.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +/* Icon Font */ +@font-face { + font-family: 'noobwrt'; + src: url('../fonts/noobwrt.woff2') format('woff2'), + url('../fonts/noobwrt.woff') format('woff'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + + diff --git a/luci-theme-noobwrt/less/layout.less b/luci-theme-noobwrt/less/layout.less new file mode 100644 index 000000000..e02f60320 --- /dev/null +++ b/luci-theme-noobwrt/less/layout.less @@ -0,0 +1,630 @@ +// out: false +// ========================================================================== +// Layout Styles +// Main layout components for LuCI admin interface +// ========================================================================== + + +// 1. Main Container Layout +// ========================================================================== + +.main { + position: relative; + width: 100%; + height: 100%; + overflow-y: auto; + display: flex; + flex-direction: row; +} + +// 2. Left Sidebar Layout +// ========================================================================== + +.main-left { + width: 15rem; + height: 100%; + flex-shrink: 0; + z-index: 100; + overflow-x: auto; + word-break: break-word; + background-color: var(--menu-bg-color); + box-shadow: rgba(0, 0, 0, 0.75) 0px 0px 15px -5px; + transition: all 0.2s; + + // Sidebar Header + .sidenav-header { + padding: 1.5rem 0.5rem; + text-align: center; + + .brand { + display: block; + margin: 0 2rem; + font-size: 1.8rem; + font-family: "TypoGraphica"; + color: var(--primary); + text-decoration: none; + text-align: center; + cursor: default; + + .logo { + max-width: 100%; + height: auto; + } + } + } + + // Navigation Menu + .nav { + margin-top: 0.5rem; + + // Top-level navigation links + &>li>a:first-child { + display: block; + position: relative; + margin: 0.1rem 0.5rem; + padding: 0.675rem 0 0.675rem 2.5rem; + font-size: 1rem; + text-decoration: none; + border-radius: 0.25rem; + cursor: default; + transition: all 0.2s; + + // Active state + &.active { + color: #fff; + background: var(--primary); + + &::before { + color: #fff !important; + } + + &::after { + transform: rotate(90deg); + color: #fff !important; + } + } + + // Hover state + &:hover { + cursor: pointer; + color: #fff; + background: var(--primary); + + &::before { + color: #fff !important; + } + } + + // Icon before text + &::before { + position: absolute; + left: 0.8rem; + padding-top: 3px; + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e915"; + color: var(--primary); + transition: all 0.3s; + } + } + + &>.slide>.menu::before { + transition: transform .1s ease-in-out; + } + + &>.slide>.menu.active::before { + transition: transform .2s ease-in-out; + } + + // Navigation list items + li { + padding: 0; + cursor: pointer; + user-select: none; + + a { + display: block; + color: var(--menu-color); + } + + // Collapsible menu items + &.slide { + padding: 0; + + ul { + display: none; + overflow: hidden; + } + + &:hover { + background: none; + } + + // Submenu container + .slide-menu { + margin: 0 0.5rem 0 2.5rem; + padding: 0rem 0.5rem; + + &.active { + display: block; + } + + li { + position: relative; + margin: 0; + border-radius: 0.25rem; + background: none; + list-style: none; + + a { + padding: 0.5rem 0rem; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + // Hover indicator line + &::after { + position: absolute; + left: 0; + bottom: 0; + width: 0; + height: 2px; + content: ""; + background-color: var(--primary); + transition: all 0.2s; + } + + &:hover { + background: none; + + &::after { + width: 100%; + } + } + } + + // Active submenu item + .active { + background: none; + color: var(--menu-color); + + a { + color: var(--menu-color); + } + + &::after { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + height: 2px; + content: ""; + background-color: var(--primary); + transition: all 0.2s; + } + + &:hover { + background: none; + + &::after { + width: 100%; + } + } + } + } + } + + // Main menu items + .menu { + display: block; + position: relative; + margin: 0.1rem 0.5rem; + padding: 0.675rem 0 0.675rem 2.5rem; + font-size: 1rem; + text-decoration: none; + border-radius: 0.25rem; + cursor: default; + transition: all 0.2s; + + // Active state + &.active { + color: #fff; + background: var(--primary); + + &::before { + color: #fff !important; + } + + &::after { + transform: rotate(90deg); + color: #fff !important; + } + } + + // Hover state + &:hover { + cursor: pointer; + color: #fff; + background: var(--primary); + + &::before { + color: #fff !important; + } + } + + // Icon before text + &::before { + position: absolute; + left: 0.8rem; + padding-top: 3px; + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e915"; + color: var(--primary); + transition: all 0.3s; + } + + // Dropdown arrow + &::after { + position: absolute; + right: 0.5rem; + top: 0.8rem; + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: '\e90f'; + color: #ced4da; + text-rendering: auto; + transition: all 0.3s; + } + } + + // Menu-specific icons and colors + .menu[data-title="Status"]:before { + content: "\e906"; + color: var(--primary); + } + + .menu[data-title="System"]:before { + content: "\e90a"; + color: #fb6340; + } + + .menu[data-title="Services"]:before { + content: "\e909"; + color: #11cdef; + } + + .menu[data-title="NAS"]:before { + content: "\e90c"; + color: #f3a4b5; + } + + .menu[data-title="VPN"]:before { + content: "\e90b"; + color: #8965e0; + } + + .menu[data-title="Network"]:before { + content: "\e908"; + color: #8965e0; + } + + .menu[data-title="Bandwidth_Monitor"]:before { + content: "\e90d"; + color: #2dce89; + } + + .menu[data-title="Docker"]:before { + content: "\e911"; + color: #6699ff; + } + + .menu[data-title="Statistics"]:before { + content: "\e913"; + color: #8965e0; + } + + .menu[data-title="Control"]:before { + content: "\e912"; + color: var(--primary); + } + + .menu[data-title="Asterisk"]:before { + content: "\e914"; + color: #fb6340; + } + + // Logout link styling + a[data-title="Log_out"], + .food[data-title="Log_out"] { + &::before { + content: "\e907"; + color: #adb5bd; + } + } + } + } + + &[style*="overflow: hidden"]>.nav>.slide>.menu::before { + display: none; + } + + // Scrollbar: hidden at rest, appears on hover + &::-webkit-scrollbar { + width: 4px; + height: 1px; + } + + &::-webkit-scrollbar-thumb { + background-color: transparent; + border-radius: 4px; + transition: background-color 0.2s; + } + + &::-webkit-scrollbar-track { + background-color: transparent; + } + + &:hover::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.18); + } + + // Firefox scrollbar + scrollbar-width: thin; + scrollbar-color: transparent transparent; + + &:hover { + scrollbar-color: rgba(0, 0, 0, 0.18) transparent; + } +} + +// 3. Right Content Area Layout +// ========================================================================== + +.main-right { + height: 100%; + flex-grow: 1; + overflow-x: hidden; + overflow-y: auto; + display: flex; + flex-direction: column; + transition: all 0.2s; + + // Main Content Container + &>#maincontent { + position: relative; + z-index: 50; + flex: 1; + display: flex; + flex-direction: column; + + &>.container { + margin: 0 1.25rem 1rem 1.25rem; + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 1rem; + } + + // Dashboard Specific Styles + .Dashboard { + color: var(--gray-dark) !important; + + h3 { + color: var(--gray-dark); + } + + p { + margin-top: 3px; + margin-bottom: 3px; + } + + hr { + border-top: 1px solid rgba(0, 0, 0, 1); + } + + .dashboard-bg { + background-color: #fff; + } + + .settings-info { + padding-top: 1em; + padding-bottom: 1em; + + p span:nth-child(2) { + max-height: 18.5px; + top: 4px; + } + + .label { + font-size: 0.7rem; + padding: 0.2rem 0.6rem; + } + } + } + } +} + +// 4. Header Layout +// ========================================================================== + +header { + position: relative; + width: 100%; + padding: 0; + color: var(--header-color); + + // Primary background modifier + &.bg-primary { + background-color: var(--primary) !important; + } + + // Header extension background + &::after { + content: ""; + position: absolute; + width: 100%; + height: 2rem; + background-color: var(--primary) !important; + } + + // Header Content Container + .fill { + padding: 0.8rem 0; + display: flex; + border-bottom: 0px solid rgba(255, 255, 255, 0.08) !important; + + .container { + width: 100%; + height: 2rem; + padding: 0 1.25rem; + display: flex; + align-items: center; + + // Left section (brand and menu toggle) + .flex1 { + flex: 1; + + .showSide { + display: none; + font-size: 1.4rem; + color: #fff; + + &:hover { + text-decoration: none; + } + } + + .brand { + display: none; + padding-left: 1rem; + font-size: 1.5rem; + font-family: "TypoGraphica"; + color: #fff; + text-decoration: none; + cursor: default; + vertical-align: text-bottom; + } + } + + // Right section (status indicators) + .pull-right { + margin-top: 0rem; + float: right; + display: flex; + } + } + + // Status Indicators + .status { + span { + display: inline-block; + margin: 0 0.25rem; + padding: 0.3rem 0.8rem; + font-size: 0.875rem; + font-weight: bold; + white-space: nowrap; + text-decoration: none; + text-transform: uppercase; + text-shadow: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.3s; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + + &:last-child { + margin-right: 0; + } + } + + span[data-indicator="poll-status"] { + color: #fff; + } + + span[data-style="active"] { + background-color: var(--green); + } + + span[data-style="inactive"] { + color: #ffffff !important; + background-color: #32325d; + } + } + } +} + +// 5. Footer Layout +// ========================================================================== + +footer { + padding: 1rem; + font-size: 0.875rem; + color: #aaa; + text-align: right; + white-space: nowrap; + overflow: hidden; + + >a { + color: #aaa; + text-decoration: none; + } +} + + +// 6. Sticky Table Headers +// ========================================================================== + +.tr.table-titles, +.tr.cbi-section-table-titles { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--background-color); +} + +// 7. View +// ========================================================================== +#view { + border-radius: 0.25rem; + display: flex; + flex-direction: column; + gap: 1rem; + + &>.spinning { + position: fixed; + top: 50%; + left: 50%; + transform: translateX(-50%) translateY(-50%); + padding: 1rem; + border-radius: 0.5rem; + background: #ffffff; + box-shadow: 0 0 1rem 0 rgba(136, 152, 170, .15); + } + + &>div:first-child { + display: flex; + flex-direction: column; + gap: 0.875rem; + + } +} diff --git a/luci-theme-noobwrt/less/normalize.less b/luci-theme-noobwrt/less/normalize.less new file mode 100644 index 000000000..e80ea1709 --- /dev/null +++ b/luci-theme-noobwrt/less/normalize.less @@ -0,0 +1,407 @@ +// out: false +// ========================================================================== +// Normalize.less +// Based on normalize.css for style reset and standardization +// ========================================================================== + +// 1. Global Reset +// ========================================================================== + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +// 2. Document Root Elements +// ========================================================================== + +html { + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; + font-family: var(--font-family-sans-serif); +} + +body { + background-color: var(--background-color); + color: var(--gray-dark); + -webkit-tap-highlight-color: transparent; + font-size: 0.875rem; +} + +// 3. Semantic Elements +// ========================================================================== + +main { + display: block; +} + +// 4. Heading Elements +// ========================================================================== + +.h1, +.h2, +.h3, +.h4, +.h5, +.h6, +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: inherit; + font-weight: normal; + line-height: 1.1 !important; + color: inherit; +} + +h1 { + font-size: 2rem; + margin: 0.67em 0; + padding-bottom: 0.5rem; + border-bottom: thin solid var(--lighter); +} + +h2 { + padding: 1rem 1.25rem; + font-size: 1.25rem; + font-weight: bold; + color: var(--gray-dark); + border-radius: 0.25rem; + background: var(--white); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); +} + +h3 { + margin: 0; + padding: 0.8755rem 1.25rem; + font-size: 1.1rem; + font-weight: bold; + line-height: 1; + color: var(--gray-dark); + border-radius: 0.25rem; + background: var(--white); + display: block; + width: 100%; +} + +h4 { + margin: 0; + padding: 0.75rem 1.25rem; + font-size: 0.875rem; + font-weight: bold; + color: var(--gray-dark-400); + + em { + padding: 0 0.5rem; + } +} + +h5 { + margin: 2rem 0 0 0; + padding-bottom: 0.5rem; + font-size: 1rem; +} + +// 5. Text Elements +// ========================================================================== + +a { + background-color: transparent; +} + +abbr { + cursor: help; + text-decoration: underline; + color: var(--primary); +} + +abbr[title] { + border-bottom: none; + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +b, +strong { + font-weight: bolder; +} + +code, +kbd, +samp { + font-family: inherit; + font-size: inherit; + padding: 1px 3px; + color: var(--dark); + border-radius: 0.5rem; + background: var(--lighter); +} + +small { + font-size: 90%; + line-height: 1.42857143; + white-space: normal; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +// 6. Horizontal Rules and Preformatted Text +// ========================================================================== + +hr { + box-sizing: content-box; + height: 0; + margin: 1rem 0; + overflow: visible; + opacity: 0.1; + border-color: var(--lighter); +} + +pre { + font-family: monospace, monospace; + font-size: 1em; + overflow: auto; +} + +// 7. Media Elements +// ========================================================================== + +img { + border-style: none; +} + +// 8. Form Elements +// ========================================================================== + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; +} + +button, +input { + overflow: visible; +} + +button, +select { + text-transform: none; +} + +// Button Styles +[type=button], +[type=reset], +[type=submit], +button { + -webkit-appearance: button; + appearance: button; +} + +[type=button]::-moz-focus-inner, +[type=reset]::-moz-focus-inner, +[type=submit]::-moz-focus-inner, +button::-moz-focus-inner { + border-style: none; + padding: 0; +} + +[type=button]:-moz-focusring, +[type=reset]:-moz-focusring, +[type=submit]:-moz-focusring, +button:-moz-focusring { + outline: 1px dotted ButtonText; +} + +// Input Styles +[type=checkbox], +[type=radio] { + box-sizing: border-box; + padding: 0; +} + +[type=number]::-webkit-inner-spin-button, +[type=number]::-webkit-outer-spin-button { + height: auto; +} + +[type=search] { + -webkit-appearance: textfield; + appearance: textfield; + outline-offset: -2px; +} + +[type=search]::-webkit-search-decoration { + -webkit-appearance: none; + appearance: none; +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + appearance: button; + font: inherit; +} + +// checkbox style + +input[type="checkbox"] { + appearance: none !important; + -webkit-appearance: none !important; + border: 1px solid var(--primary); + + width: 1rem !important; + height: 1rem !important; + padding: 0; + cursor: pointer; + transition: all 0.2s; +} + +input[type="checkbox"]:checked { + border: 1px solid var(--primary); + background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 8 8\'%3e%3cpath fill=\'%23fff\' d=\'M6.564.75l-3.59 3.612-1.538-1.55L0 4.26 2.974 7.25 8 2.193z\'/%3e%3c/svg%3e') !important; + background-color: var(--primary); + background-size: 70%; + background-repeat: no-repeat; + background-position: center; +} + +// Select Styles +select { + padding: 0.36rem 0.8rem; + color: var(--gray-dark); + border: thin solid var(--lighter); + background-color: var(--white); + background-image: none; +} + +// Textarea Styles +textarea { + padding: 0.25rem; + overflow: auto; + + &:focus-visible { + outline: none; + border: 1px solid var(--primary); + } +} + +// Fieldset Styles +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal; +} + +// 9. Interactive Elements +// ========================================================================== + +details { + display: block; +} + +summary { + display: list-item; +} + +template { + display: none; +} + +progress { + vertical-align: baseline; +} + +// 10. List Elements +// ========================================================================== + +ul { + line-height: normal; +} + +li { + list-style-type: none; +} + +// 11. State Styles +// ========================================================================== + +[disabled="disabled"] { + pointer-events: none; +} + + +::selection { + background-color: var(--primary); + color: var(--white); +} + +::placeholder { + color: var(--lighter); +} + +a:link, +a:visited, +a:active { + color: var(--primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +// 12 tools +// ========================================================================== + +.pull-right { + float: right; +} + +.pull-left { + float: left; +} + +.nowrap:not(.td) { + white-space: nowrap; +} + +div[style="width:100%;height:300px;border:1px solid #000;background:#fff"] { + border: 0 !important; +} \ No newline at end of file diff --git a/luci-theme-noobwrt/less/page-fix.less b/luci-theme-noobwrt/less/page-fix.less new file mode 100644 index 000000000..57392dd0d --- /dev/null +++ b/luci-theme-noobwrt/less/page-fix.less @@ -0,0 +1,1026 @@ +// out: false +// ========================================================================== +// Page Fix Styles +// Page-specific CSS fixes and customizations for LuCI admin interface +// ========================================================================== + +// 1. System Administration Pages +// ========================================================================== + +// System - Auto Reboot +[data-page="admin-system-autoreboot"] { + #cbi-autoreboot { + margin-top: 0; + } +} + +// System - System Settings +[data-page="admin-system-system"] { + .control-group { + gap: .875rem; + } + + .cbi-dynlist { + margin: 0.25rem 0; + } +} + +// System - Admin Password +[data-page="admin-system-admin"] .cbi-map h2, +[data-page="admin-system-admin-password"] .cbi-map h2, +[data-page="admin-system-admin"] .cbi-map .cbi-map-descr, +[data-page="admin-system-admin-password"] .cbi-map .cbi-map-descr { + margin-left: 0; + color: var(--gray-dark); +} + +[data-page="admin-system-admin"], +[data-page="admin-system-admin-password"] { + .cbi-section-node { + padding: 1rem 0; + } +} + +// System - SSH Keys +[data-page="admin-system-admin-sshkeys"] { + .cbi-dynlist { + margin-left: 1rem; + } +} + +// System - Startup Scripts +[data-page="admin-system-startup"] { + #view { + &>div:first-child { + div[data-initialized="true"] { + margin-top: -0.4375rem; + } + } + } + + [data-tab-title] p { + margin-left: 0; + margin-bottom: 0; + display: flex; + align-items: center; + justify-content: center; + } + + textarea { + line-height: 1.25; + overflow-y: auto; + width: 100%; + min-height: 15rem; + margin: 0 1rem; + padding: 1rem; + resize: none; + color: #8898aa; + border-radius: 0.25rem; + border: 1px solid #dee2e6; + + &:focus-visible { + outline: none; + box-shadow: none; + border: 1px solid var(--primary); + } + } +} + +// System - Crontab +[data-page="admin-system-crontab"] { + #view p { + textarea { + line-height: 1.25; + overflow-y: hidden; + width: 100%; + min-height: 15rem; + padding: 1rem; + resize: none; + background-color: transparent; + background: var(--white); + outline: none; + color: #8898aa; + border-radius: 0.25rem; + border: 1px solid #dee2e6; + transition: all 0.2s ease; + + &:focus-visible { + outline: none; + box-shadow: none; + border: 1px solid var(--primary); + } + } + } +} + +// System - Crontab Helper +[data-page="admin-system-crontabhelper"] { + .crontab-row { + .dropdown-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + + div { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + } + } + } +} + +// System - Partexp +[data-page="admin-system-partexp-global"] { + .cbi-section { + padding: 1rem 0; + } +} + +// System - Attended Sysupgrade +[data-page="admin-system-attendedsysupgrade"] #view .cbi-button { + margin-left: 0 !important; + margin-top: 1rem !important; +} + +[data-page="admin-system-attendedsysupgrade-configuration"] { + .cbi-map { + .cbi-map-descr { + padding-bottom: 0; + } + } +} + +// System - Flash Firmware +[data-page="admin-system-flash"] { + .cbi-value { + padding: 0 1rem; + } + + .cbi-section { + .cbi-section { + margin-top: 0; + } + } + + .cbi-map-tabbed { + border-radius: 0.25rem; + } + + .cbi-section-node { + padding-top: 0; + padding-bottom: 0.5rem; + } + + legend { + font-size: 1.2rem; + width: 100%; + display: block; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + line-height: 1.5; + margin-bottom: 0; + letter-spacing: 0.1rem; + color: #32325d; + font-weight: bold; + padding: 1rem 0 1rem 1rem; + } + + .cbi-section-descr { + font-weight: 600; + padding: 1rem 0 1rem 1rem; + color: #525f7f; + } + + .cbi-page-actions { + padding: 0rem 1rem 1rem 0rem; + } + + .modal { + label>input[type="checkbox"] { + top: -0.25rem; + } + + .btn { + white-space: normal !important; + } + } + + .modal label>input[type="checkbox"] { + vertical-align: text-top; + top: auto; + } +} + +// System - File Transfer +[data-page="admin-system-filetransfer"] { + #cbi-upload { + margin-top: 0; + } + + .cbi-section-table { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.03); + } +} + +// System - File Assistant +[data-page="admin-system-fileassistant"] { + #upload-toggle { + display: none !important; + } + + .fb-container .panel-title { + padding: 0.5rem 0.75rem !important; + } + + .cbi-section.fb-container { + padding: 0.5rem; + + label.cbi-value-title { + line-height: 1.5rem; + padding: 0.5rem 0.75rem; + width: 60px; + text-align: left; + } + + div.cbi-value-field { + width: 100%; + display: block; + padding-left: 60px; + } + + div#list-content { + table.cbi-section-table { + + thead, + tbody { + td.cbi-section-table-cell { + width: 232px; + } + } + } + + tbody { + tr:nth-child(1) { + td.parent-icon { + strong { + margin-left: 0 !important; + } + } + } + + td.cbi-value-field.file-icon, + td.cbi-value-field.folder-icon { + strong { + vertical-align: middle; + margin-left: 5px; + } + } + } + } + } + + .fb-container .panel-container { + border-bottom-color: #dee2e6; + display: flex; + + .panel-title { + flex: 1; + } + } + + .fb-container .upload-container { + display: flex !important; + + .upload-file { + flex: 1; + margin-right: 0 !important; + } + } +} + +// System - Package Manager (Software) +[data-page="admin-system-opkg"] { + h2 { + margin-left: 0; + color: var(--gray-dark); + } + + input[name="filter_i18n"] { + top: 2px; + vertical-align: text-top; + } + + div.btn { + line-height: 3; + display: inline; + padding: 0.3rem 0.6rem; + } + + #maincontent>.container { + margin: 0 1.25rem 1rem 1.25rem; + margin-bottom: 1rem; + } +} + +[data-page="admin-system-package-manager"] { + .controls { + div:nth-child(5) { + >label { + margin-bottom: 5px; + } + + >div { + label { + margin-top: 1rem; + + input { + top: -2px; + right: 0; + vertical-align: middle; + } + } + } + } + } +} + +// System - Power Management +[data-page="admin-system-reboot"] { + p { + padding-left: 1.5rem; + } + + p>span { + position: relative; + top: 0.1rem; + left: 1rem; + } + + .cbi-button { + background: #fb6340 !important; + border-color: #fb6340 !important; + margin-left: 0 !important; + } + + .cbi-button.cbi-button-action.important { + align-self: flex-end; + } +} + +/* applyreboot fix */ +#applyreboot-container { + margin: 2rem; +} + +#applyreboot-section { + line-height: 300%; + margin: 2rem; +} + +[data-page="admin-system-poweroff"] { + .container { + h2+br+p { + margin-bottom: 1rem; + padding-left: 1.5rem; + } + } +} + +[data-page="admin-system-poweroffdevice"] { + .container { + h2 { + margin: 0; + } + + h2+p { + margin-bottom: 1rem; + padding-left: 1.5rem; + } + + button+div { + display: none; + } + } + + + .btn.cbi-button.cbi-button-negative { + align-self: flex-end; + } +} + +// 2. Network Configuration Pages +// ========================================================================== + +// Network - Interfaces +[data-page="admin-network-network"] { + .cbi-value-field { + .cbi-dynlist { + padding: 0 !important; + } + } + + .td>.ifacebadge>.cbi-tooltip-container { + display: flex; + + img { + vertical-align: middle; + } + } + + div[data-name="_gen_server_keypair"], + div[data-name="_gen_peer_keypair"], + div[data-name="_gen_psk"] { + .cbi-value-title { + height: 2.4rem; + } + } + + #modal_overlay>.modal.cbi-modal>div>p>textarea { + height: 20em !important; + border: 1px solid #dee2e6 !important; + border-radius: 4px; + } +} + +// Network - DHCP and DNS +[data-page="admin-network-dhcp"] { + .cbi-value { + padding: 0; + } + + [data-tab-active="true"] { + padding: 1rem 1rem !important; + } +} + +// Network - Diagnostics +[data-page="admin-network-diagnostics"] { + .table { + box-shadow: none; + } + + .cbi-section { + padding: 1rem; + font-family: monospace; + background: #fff !important; + } + + textarea { + background: transparent; + border-radius: 0.25rem; + color: #8898aa; + border: 1px solid #dee2e6; + padding: 0.5rem; + } + + .tr { + display: flex; + flex-direction: row; + + .td { + background-color: #fff !important; + border-bottom: 1px solid #dee2e6 !important; + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + + input { + margin: 0 !important; + border-right: none !important; + border-bottom-right-radius: 0 !important; + border-top-right-radius: 0 !important; + } + + .cbi-dropdown, + .cbi-button { + height: 2.5rem; + border-left: none !important; + border-bottom-left-radius: 0 !important; + border-top-left-radius: 0 !important; + + .more { + display: block; + width: 1px; + height: 100%; + overflow: hidden; + text-indent: 5000px; + background-color: var(--white) + } + } + } + } + + + .cbi-dropdown { + .open { + color: var(--white); + } + } +} + +// Network - Firewall +[data-page="admin-network-firewall-custom"], +[data-page="admin-status-routes"] { + #view { + p { + padding: 0 1.25rem; + + textarea { + padding: 1rem; + border-radius: 0.25rem; + } + } + + &>h3 { + border-radius: 0.25rem 0.25rem 0 0; + } + + .cbi-tabmenu+div { + margin-top: -0.5rem; + } + } +} + +// 3. Status Pages +// ========================================================================== + +.cbi-section.fade-in { + .cbi-title { + position: relative; + + > div:last-child { + position: absolute; + right: 1.25rem; + top: 0; + + span { + font-size: 0; + background: none!important; + color: #ced4da!important; + + &:after { + transition: all .3s; + font-family: 'noobwrt' !important; + content: '\e90f'; + font-size: 1.1rem; + padding: .8755rem 0; + position: absolute; + right: 0; + } + + &[data-style=inactive]:after { + transform: rotate(90deg); + } + } + } + } +} + +// Status - Overview +[data-page="admin-status"], +[data-page="admin-status-overview"] { + #view > div:first-child { + gap: 0 !important; + } +} + +// Status - IPTables +[data-page="admin-status-iptables"] { + .right { + margin-bottom: 0 !important; + } +} + +// Status - NFTables +[data-page="admin-status-nftables"] { + .nft-chain-hook { + padding: 0.5rem 1.25rem; + } + + // Fixed the issue of .alert-message not closing properly. + .alert-message { + opacity: 1; + } + .alert-message.fade-out { + opacity: 0; + } +} + +// Status - Network Bandwidth Monitor +[data-page="admin-services-nlbw-display"] { + .cbi-section[data-tab="export"] { + padding: 1.5rem !important; + } + .cbi-section > .right { + margin: 15px 15px 0px 0px; + } +} + +// 4. Services Pages +// ========================================================================== + +// Services - Dynamic DNS +[data-page="admin-services-ddns"] { + #syslog { + overflow: auto; + font-size: 12px !important; + } +} + +// Services - TTY Daemon +[data-page="admin-services-ttyd"] { + .container { + display: flex; + flex-direction: column; + } + + #view { + flex: 1; + + iframe { + height: 100%; + } + } +} + +// Services - Dockerman +#cbi-dockerd { + .cbi-section { + box-sizing: border-box; + padding: 16px; + } + + .cbi-section.cbi-tblsection { + padding: 0; + } + +} + +// Services - SmartDNS +#cbi-smartdns { + #service_status { + padding: 16px; + } + + .cbi-section-node { + padding: 16px; + } +} + + +#cbi-docker { + .cbi-section { + .cbi-section-node { + box-sizing: border-box; + padding: 16px; + } + } +} + +// Services - NPC +[data-page="admin-services-npc"] { + div[id="cbid.npc.config.vkey"] { + white-space: normal; + + input[id="widget.cbid.npc.config.vkey"][type="password"] { + margin-top: 0; + margin-bottom: 0; + } + + .cbi-button { + margin: 0; + border: none; + } + } + + div[id^="cbid.npc."][id$=".server_addr"]:not([id="cbid.npc.config.server_addr"]), + div[id^="cbid.npc."][id$=".vkey"]:not([id="cbid.npc.config.vkey"]), + div[id^="cbid.npc."][id$=".protocol"]:not([id="cbid.npc.config.protocol"]), + div[id^="cbid.npc."][id$=".dns"]:not([id="cbid.npc.config.dns"]) { + +br { + display: none; + } + } + + div.cbi-value.nowrap { + white-space: normal; + } +} + +// Services - OpenClash +[data-page^="admin-services-openclash"] { + .oc { + --bg-light: #fff; + --bg-gray: #fff; + border-radius: 8px; + + .developer-container { + background: transparent; + } + + .announcement-card { + width: auto!important; + margin: 10px!important; + } + } + + #cbi-openclash>fieldset { + background: none; + padding: 0; + + >table>tbody>tr>td { + border: none; + padding: 0; + } + + .main-card { + border: none; + box-shadow: none; + padding: 0 10px 10px 10px; + background: transparent; + } + + .myip-main-card { + margin: 0; + padding: 10px; + border: none; + background: transparent; + + .myip-section-title { + margin: 0; + border-width: 1px; + } + } + } + + .sub-card .card-title { + margin-top: 5px; + } + + .oc .main-cards-container { + margin: 0 !important; + gap: 0 !important; + } + + .oc .config-file-bottom { + margin-bottom: 5px; + + .card-actions { + margin-bottom: 2px !important; + } + } + + .oc .card-content { + align-items: center !important; + justify-content: center !important; + } + + .oc .card-controls { + align-self: center; + } + + .oc .core-main-controls { + justify-content: center !important; + } + + .oc .plugin-toggle-container { + margin-left: inherit !important; + } + + .oc .core-status-toggle { + flex: 0 !important; + min-width: auto !important; + } + + .oc .config-upload-content { + background: #fff !important; + } + + .oc .subscription-info-container { + background: transparent !important; + } + + .cbi-tabmenu>li { + border-right: none !important; + margin: 0 0.4rem 0 0 !important; + + &:last-child { + margin-right: 0 !important; + } + } + + #tab-content .dom { + padding: 0 1rem 1rem 1rem; + } + + .cbi-input-file { + padding: 0.2813rem; + width: 15rem !important; + } + + [id="container.openclash.config.debug"] fieldset { + border: none !important; + padding: 1rem !important; + } + + #diag-rc-output>pre, + #dns-rc-output>pre { + font-size: 0.875rem; + color: #8898aa; + border: 1px solid #dee2e6; + background-color: transparent; + border-radius: 0.25rem; + font-family: 'Google Sans' !important; + box-shadow: none; + } + + #debug-rc-output>textarea { + font-family: 'Google Sans' !important; + } + + .CodeMirror { + font-size: inherit; + font-family: 'Google Sans' !important; + } + + .cbi-button-up, + .cbi-button-down { + padding: 0.8rem 1.5rem; + background-color: #f1f1f1; + font-size: 0; + } + + select#CORE_VERSION, + select#RELEASE_BRANCH { + width: auto; + } +} + +// 5. VPN Pages +// ========================================================================== + +// VPN - Passwall +[data-page="admin-vpn-passwall"] h4 { + background: transparent; +} + +/* openvpn bug fix */ +.OpenVPN a { + line-height: initial !important; +} + + +// 6. Other Pages +// ========================================================================== + +// wireless +/* wireless overview */ +#cbi-wireless { + >#wifi_assoclist_table { + >.tr { + box-shadow: inset 1px -1px 0 #ddd, inset -1px -1px 0 #ddd; + + &.placeholder { + >.td { + right: 2rem; + bottom: 2rem; + left: 2rem; + border-top: thin solid #ddd !important; + } + } + + &.table-titles { + box-shadow: inset 1px 0 0 #ddd, inset -1px 0 0 #ddd; + + >.th { + border-bottom: thin solid #ddd; + box-shadow: 0 -1px 0 0 #ddd; + } + } + } + } +} + +#wifi_assoclist_table { + >.tr { + >.td[data-title="RX Rate / TX Rate"] { + width: 23rem; + } + } +} + +/* samba */ +#cbi-samba [data-tab="template"] { + .cbi-value-field { + display: block; + } + + .cbi-value-title { + width: auto; + padding-bottom: .6rem; + } +} + +/* fix status */ +.node-status-overview>.main fieldset:nth-child(4) .td:nth-child(2), +.node-status-processes>.main .table .tr .td:nth-child(3) { + white-space: normal; +} + +div[style*="display:grid;grid-template-columns:repeat"] { + display: flex !important; + justify-content: space-evenly !important; + flex-wrap: wrap; + + .ifacebox { + text-align: center; + flex-basis: 100px; + + .ifacebox-body { + font-size: 0.7rem; + padding: 0.875rem; + + .cbi-tooltip-container { + font-size: inherit !important; + } + } + } +} + +/* firewall */ +#iptables { + margin: 0; +} + +.Firewall form { + margin: 2rem 2rem 0 0; + padding: 0; + box-shadow: none; +} + +#cbi-firewall-redirect, +#cbi-network-switch_vlan, +#cbi-firewall-zone { + table { + * { + font-size: small; + } + + input[type="text"] { + width: 5rem; + } + + select { + min-width: 3.5rem; + } + } +} + +#cbi-network-switch_vlan { + + .th, + .td { + flex-basis: 12%; + } + + .table { + display: block; + } + + .td { + width: 100%; + } +} + +#cbi-firewall-zone { + .table { + display: block; + } + + .td { + width: 100%; + } +} + +[data-page^="admin-system-commands"] .panel-title, +[data-page^="command-cfg"] .mobile-hide, +[data-page^="command-cfg"] .showSide { + display: none; +} + +// overview & mwan3 +#mwan3-service-status { + display: flex; + justify-content: center; + align-items: center; + flex-wrap: wrap; +} + +#mwan3-service-status > .alert-message { + position: static; + transform: none; +} + diff --git a/luci-theme-noobwrt/less/pure-min.less b/luci-theme-noobwrt/less/pure-min.less new file mode 100644 index 000000000..c33828dac --- /dev/null +++ b/luci-theme-noobwrt/less/pure-min.less @@ -0,0 +1,245 @@ +// out: false +/** +Pure v3.0.0 +Copyright 2013 Yahoo! +Licensed under the BSD License. +https://github.com/pure-css/pure/blob/master/LICENSE +*/ +/** +normalize.css v | MIT License | https://necolas.github.io/normalize.css/ +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/** normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +.pure-g { + display: flex; + flex-flow: row wrap; + align-content: flex-start +} + +.pure-u { + display: inline-block; + vertical-align: top +} + +.pure-u-1, +.pure-u-1-1, +.pure-u-1-12, +.pure-u-1-2, +.pure-u-1-24, +.pure-u-1-3, +.pure-u-1-4, +.pure-u-1-5, +.pure-u-1-6, +.pure-u-1-8, +.pure-u-10-24, +.pure-u-11-12, +.pure-u-11-24, +.pure-u-12-24, +.pure-u-13-24, +.pure-u-14-24, +.pure-u-15-24, +.pure-u-16-24, +.pure-u-17-24, +.pure-u-18-24, +.pure-u-19-24, +.pure-u-2-24, +.pure-u-2-3, +.pure-u-2-5, +.pure-u-20-24, +.pure-u-21-24, +.pure-u-22-24, +.pure-u-23-24, +.pure-u-24-24, +.pure-u-3-24, +.pure-u-3-4, +.pure-u-3-5, +.pure-u-3-8, +.pure-u-4-24, +.pure-u-4-5, +.pure-u-5-12, +.pure-u-5-24, +.pure-u-5-5, +.pure-u-5-6, +.pure-u-5-8, +.pure-u-6-24, +.pure-u-7-12, +.pure-u-7-24, +.pure-u-7-8, +.pure-u-8-24, +.pure-u-9-24 { + display: inline-block; + letter-spacing: normal; + word-spacing: normal; + vertical-align: top; + text-rendering: auto +} + +.pure-u-1-24 { + width: 4.1667% +} + +.pure-u-1-12, +.pure-u-2-24 { + width: 8.3333% +} + +.pure-u-1-8, +.pure-u-3-24 { + width: 12.5% +} + +.pure-u-1-6, +.pure-u-4-24 { + width: 16.6667% +} + +.pure-u-1-5 { + width: 20% +} + +.pure-u-5-24 { + width: 20.8333% +} + +.pure-u-1-4, +.pure-u-6-24 { + width: 25% +} + +.pure-u-7-24 { + width: 29.1667% +} + +.pure-u-1-3, +.pure-u-8-24 { + width: 33.3333% +} + +.pure-u-3-8, +.pure-u-9-24 { + width: 37.5% +} + +.pure-u-2-5 { + width: 40% +} + +.pure-u-10-24, +.pure-u-5-12 { + width: 41.6667% +} + +.pure-u-11-24 { + width: 45.8333% +} + +.pure-u-1-2, +.pure-u-12-24 { + width: 50% +} + +.pure-u-13-24 { + width: 54.1667% +} + +.pure-u-14-24, +.pure-u-7-12 { + width: 58.3333% +} + +.pure-u-3-5 { + width: 60% +} + +.pure-u-15-24, +.pure-u-5-8 { + width: 62.5% +} + +.pure-u-16-24, +.pure-u-2-3 { + width: 66.6667% +} + +.pure-u-17-24 { + width: 70.8333% +} + +.pure-u-18-24, +.pure-u-3-4 { + width: 75% +} + +.pure-u-19-24 { + width: 79.1667% +} + +.pure-u-4-5 { + width: 80% +} + +.pure-u-20-24, +.pure-u-5-6 { + width: 83.3333% +} + +.pure-u-21-24, +.pure-u-7-8 { + width: 87.5% +} + +.pure-u-11-12, +.pure-u-22-24 { + width: 91.6667% +} + +.pure-u-23-24 { + width: 95.8333% +} + +.pure-u-1, +.pure-u-1-1, +.pure-u-24-24, +.pure-u-5-5 { + width: 100% +} +.col-1 { + flex: 1 1 30px !important; +} + +.col-2 { + flex: 2 2 60px !important; +} + +.col-3 { + flex: 3 3 90px !important; +} + +.col-4 { + flex: 4 4 120px !important; +} + +.col-5 { + flex: 5 5 150px !important; +} + +.col-6 { + flex: 6 6 180px !important; +} + +.col-7 { + flex: 7 7 210px !important; +} + +.col-8 { + flex: 8 8 240px !important; +} + +.col-9 { + flex: 9 9 270px !important; +} + +.col-10 { + flex: 10 10 300px !important; +} \ No newline at end of file diff --git a/luci-theme-noobwrt/less/responsive.less b/luci-theme-noobwrt/less/responsive.less new file mode 100644 index 000000000..0c5dc60be --- /dev/null +++ b/luci-theme-noobwrt/less/responsive.less @@ -0,0 +1,641 @@ +// out: false + +// ========================================================================== +// Responsive Styles +// Mobile-first responsive design for LuCI admin interface +// ========================================================================== + +// 1. Large Desktop Screens (1600px and below) +// ========================================================================== + +@media screen and (max-width: 1600px) { + + // Header adjustments + header>.fill>.container>#logo { + margin: 0 2.5rem 0 0.5rem; + } + + // Sidebar width + .main-left { + width: 13rem; + } + + // Button and label sizing + .btn:not(button), + .label { + padding: 0.5rem 0.75rem; + } + + // Form field sizing + .cbi-value-title { + width: 15rem; + padding-right: 0.6rem; + } + + .cbi-value-field .cbi-dropdown, + .cbi-value-field .cbi-input-select, + .cbi-value input[type="text"], + .cbi-value input[type="password"], + .cbi-value textarea { + min-width: 18rem; + } + + // Specific component adjustments + #cbi-firewall-zone .cbi-input-select { + min-width: 9rem; + } + + .cbi-input-textarea { + font-size: small; + } + + .node-admin-status>.main fieldset li>a { + padding: 0.3rem 0.6rem; + } +} + +// 3. Standard Desktop Screens (1366px and below) +// ========================================================================== + +@media screen and (max-width: 1366px) { + + // Header adjustments + header>.fill>.container { + cursor: default; + } + + // Sidebar width + .main-left { + width: 13rem; + } + + // Tab styling + .tabs>li>a, + .cbi-tabmenu>li>a { + padding: 0.2rem 0.8rem; + } + + // Panel and table adjustments + .panel-title { + font-size: 1.1rem; + padding-bottom: 1rem; + } + + table { + width: 100% !important; + } + + .table .cbi-input-text { + width: 100%; + } + + // Form field sizing + .cbi-value-field .cbi-dropdown, + .cbi-value-field .cbi-input-select, + .cbi-value input[type="text"], + .cbi-value input[type="password"] { + min-width: 16rem; + } + + #cbi-firewall-zone .cbi-input-select { + min-width: 5.5rem; + } + + // Navigation font sizes + .main>.main-left>.nav>li, + .main>.main-left>.nav>li>a, + .main .main-left .nav>li>a:first-child, + .main>.main-left>.nav>.slide>.menu, + .main>.main-left>.nav>li>[data-title="Log_out"] { + font-size: 0.9rem; + } + + .main>.main-left>.nav>.slide>.slide-menu>li>a { + font-size: 0.875rem; + } + + // Modal adjustments + #modal_overlay { + top: 0rem; + } + + // Page-specific table fixes + [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table) { + display: block; + } + + [data-page="admin-network-firewall-forwards"] .table:not(.cbi-section-table), + [data-page="admin-network-firewall-rules"] .table:not(.cbi-section-table), + [data-page="admin-network-hosts"] .table, + [data-page="admin-network-routes"] .table { + overflow-y: visible; + } + + // Button sizing + .btn:not(button), + .cbi-button { + font-size: 0.875rem; + } +} + +// 4. Small Desktop/Large Tablet (1152px and below) +// ========================================================================== + +@media screen and (max-width: 1152px) { + + // Header adjustments + header>.fill>.container>#logo { + display: none; + } + + header>.fill>.container>.brand { + position: relative; + } + + // Layout adjustments + html, + .main { + overflow-y: visible; + } + + .main>.loading>span { + top: 25%; + } + + .main-left { + width: 13rem; + } + + // Login page specific + body:not(.logged-in) .showSide { + visibility: hidden; + width: 0; + margin: 0; + } + + .node-main-login>.main .cbi-value-title { + text-align: left; + } + + // Form field adjustments + .cbi-value-title { + width: 12rem; + padding-right: 1rem; + } + + .cbi-value-field .cbi-dropdown, + .cbi-value-field .cbi-input-select, + .cbi-value input[type="text"] { + width: 16rem; + min-width: 16rem; + } + + // Password field specific sizing + .cbi-value input[name^="pw"], + .cbi-value input[data-update="change"]:nth-child(2) { + width: 13rem !important; + min-width: 13rem; + } + + // Code output adjustments + #diag-rc-output>pre, + #command-rc-output>pre, + [data-page="admin-services-wol"] .notice code { + font-size: 1rem; + } + + // Table responsive behavior + .table { + display: block; + } + + .Interfaces .table { + overflow-x: hidden; + } + + #packages.table { + display: grid; + } + + // Table row adjustments + .tr { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + + .Overview .table[width="100%"]>.tr { + flex-wrap: nowrap; + } + + .tr.placeholder { + border-bottom: thin solid #ddd; + } + + .tr.placeholder>.td, + #cbi-firewall .tr>.td, + #cbi-network .tr:nth-child(2)>.td, + .cbi-section #wifi_assoclist_table .tr>.td { + border-top: 0; + } + + // Table cell adjustments + .th, + .td { + display: inline-block; + align-self: flex-start; + flex: 2 2 10%; + text-overflow: ellipsis; + word-wrap: break-word; + } + + .td select, + .td input[type="text"] { + width: 100%; + word-wrap: normal; + } + + .td [data-dynlist]>input, + .td input.cbi-input-password { + width: calc(100% - 1.5rem); + } + + .td[data-type="button"], + .td[data-type="fvalue"] { + flex: 1 1 12.5%; + text-align: left; + } + + .th.cbi-value-field, + .td.cbi-value-field, + .th.cbi-section-table-cell, + .td.cbi-section-table-cell { + flex-basis: auto; + padding-top: 1rem; + } + + // Section table adjustments + .cbi-section-table-row { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + + .td.cbi-value-field, + .cbi-section-table-cell { + display: inline-block; + flex: 10 10 auto; + flex-basis: 50%; + text-align: center; + } + + .td.cbi-section-actions { + vertical-align: bottom; + } + + // Hide table headers on mobile + .tr.table-titles, + .tr.cbi-section-table-titles, + .tr.cbi-section-table-descr { + display: none; + } + + .tr[data-title]::before, + .tr.cbi-section-table-titles.named::before { + font-size: 0.9rem; + display: block; + flex: 1 1 100%; + border-bottom: thin solid rgba(0, 0, 0, 0.26); + background: #e9ecef; + } + + .td[data-title], + [data-page^="admin-status-realtime"] .td[id] { + text-align: left; + } + + .td[data-title]::before { + display: block; + } + + // Button spacing + .cbi-button+.cbi-button { + margin-left: 0; + } + + .td.cbi-section-actions>*>*, + .td.cbi-section-actions>*>form>* { + margin: 2.1px 3px; + } + + // Firewall form adjustments + .Firewall form { + position: static !important; + margin: 0 0 2rem 0; + padding: 2rem; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + + input { + width: 100% !important; + margin: 0; + margin-top: 1rem; + } + } + + .Firewall .center, + .Firewall .center::before { + text-align: left !important; + } + + // Button sizing + .btn:not(button), + .cbi-button { + font-size: 0.875rem; + } +} + +// 5. Tablet Portrait (768px and below) +// ========================================================================== + +@media screen and (max-width: 768px) { + + // Base font size + body { + font-size: 0.875rem; + } + + // Progress bar adjustments + .cbi-progressbar::after { + font-size: 0.6rem; + } + + // Mobile sidebar behavior + .main-left { + position: fixed; + z-index: 100; + width: 0; + + &.active { + width: 13rem; + } + } + + .main-right { + width: 100%; + + &.active { + overflow-y: hidden; + } + } + + .darkMask.active { + display: block; + } + + // Mobile menu toggle + .showSide { + padding: 0.1rem; + position: relative; + z-index: 99; + display: inline-block !important; + visibility: visible !important; + width: auto !important; + margin: 0 !important; + + &::before { + font-family: 'noobwrt' !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + content: "\e20e"; + font-size: 1.7rem; + } + } + + // Brand visibility + header>.fill>.container>.flex1>.brand { + display: inline-block; + } + + // Navigation font size + .main>.main-left>.nav>.slide>.slide-menu>li>a { + font-size: 0.875rem; + } +} + +// 6. Mobile Portrait (600px and below) +// ========================================================================== + +@media screen and (max-width: 600px) { + + // Hide mobile elements + .mobile-hide { + display: none; + } + + // Content margins + #maincontent>.container { + margin: 0 1rem 1rem 1rem; + } + + // Form adjustments + .cbi-value-title { + text-align: left; + } + + .cbi-dynlist p { + padding: 0.5rem 1rem; + } + + // Prevent horizontal scroll + body { + overflow-x: hidden; + } + + // Login page adjustments + .node-main-login .main .main-right #maincontent .container .cbi-map .cbi-section .cbi-section-node .cbi-value .cbi-value-field { + width: 16rem; + } + + .node-main-login footer { + display: none; + } + + // Tab scrollbar hiding + .tabs, + .cbi-tabmenu { + &::-webkit-scrollbar { + width: 0px; + height: 0px; + } + } + + // Form field layout + .cbi-value-field, + .cbi-value-description { + display: block !important; + padding-left: 0 !important; + padding-right: 0 !important; + } + + [data-page="admin-system-admin-password"] .cbi-value-field { + display: table-cell !important; + } + + // Modal adjustments + .modal.cbi-modal { + max-width: 100%; + max-height: none; + } + + .modal { + display: flex; + align-items: center; + flex-wrap: wrap; + width: 100%; + min-width: 270px; + max-width: 600px; + min-height: 32px; + margin: 5em auto; + padding: 1em; + border-radius: 3px !important; + background: #fff; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.16), 0 0 2px 0 rgba(0, 0, 0, 0.12); + } + + // Dropdown adjustments + .cbi-dropdown[open]>ul.dropdown { + margin-bottom: 1rem; + } + + // Login page footer + .login-page .login-container footer { + display: none; + } +} + +// 7. Small Mobile (480px and below) +// ========================================================================== + +@media screen and (max-width: 480px) { + + // Hide mobile elements + .mobile-hide { + display: none; + } + + // Interface box adjustments for overview page + div[style*="display:grid;grid-template-columns:repeat"] { + .ifacebox { + flex-basis: 80px; + + .ifacebox-body { + padding: 0.875rem 0.5rem; + font-size: 0.6rem; + } + } + } + + // Login page mobile adjustments + .login-page .login-container { + margin-left: 0rem !important; + width: 100%; + padding: 1.5rem 1rem; + + .login-form { + max-width: 22rem; + + .login-card { + padding: 0; + + &.split { + grid-template-columns: 1fr; + } + + .login-hero { + padding: 2rem 1.5rem 1.2rem; + + .hero-orbit { + display: none; + } + + .hero-particles { + span { + width: 6px; + height: 6px; + box-shadow: none; + } + + span:nth-child(2), + span:nth-child(4), + span:nth-child(12) { + width: 8px; + height: 8px; + } + } + + .hero-icon { + width: 7.5rem; + height: 7.5rem; + border-radius: 50%; + + img { + width: 5.1rem; + height: 5.1rem; + } + } + + .hero-brand { + max-width: 9rem; + } + } + + .login-panel { + padding: 1.8rem 1.5rem 1.5rem; + } + } + + .form-login { + .input-group { + &::before { + color: var(--primary); + } + } + } + } + } +} + +// 8. Desktop Scrollbar Styling (600px and above) +// ========================================================================== + +@media screen and (min-width: 600px) { + ::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + ::-webkit-scrollbar, + ::-webkit-scrollbar-corner { + background: transparent; + } + + ::-webkit-scrollbar-thumb { + background: var(--primary); + border-radius: 10px; + + &:hover { + background: var(--primary); + } + + &:active { + background: var(--primary); + } + } +} diff --git a/luci-theme-noobwrt/less/sysauth.less b/luci-theme-noobwrt/less/sysauth.less new file mode 100644 index 000000000..9e37aa11d --- /dev/null +++ b/luci-theme-noobwrt/less/sysauth.less @@ -0,0 +1,452 @@ +// out: false + +// ========================================================================== +// Login Page Styles +// Optimized with LESS best practices +// ========================================================================== + +// Mixins for common patterns +.flex-center() { + display: flex; + align-items: center; + justify-content: center; +} + +.absolute-full() { + position: absolute; + top: 0; + left: 0; + height: 100vh; + width: 100vw; +} + +.icon-base() { + font-family: 'noobwrt' !important; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.login-page { + position: relative; + display: flex; + min-height: 100vh; + flex-direction: column; + font-family: 'Google Sans', 'Segoe UI', Tahoma, sans-serif; + color: var(--default); + + &::before { + content: none; + } + + // Background video container + .video { + .absolute-full(); + .flex-center(); + background-color: var(--darker); + overflow: hidden; + z-index: 0; + + video { + width: 100%; + height: auto; + } + } + + // Volume control button + .volume-control { + position: fixed; + top: 1rem; + right: 1rem; + width: 1.5rem; + height: 1.5rem; + z-index: 5000; + cursor: pointer; + background: url(../img/volume_high.svg) no-repeat center/contain; + + &.mute { + background-image: url(../img/volume_off.svg); + } + } + + // Main background image + .main-bg { + .absolute-full(); + background: url(../img/blank.png) no-repeat center/cover; + transition: all 0.5s ease; + z-index: 0; + } + + // Login container + .login-container { + z-index: 2; + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + width: 100%; + padding: 2rem 1.5rem; + + // Login form + .login-form { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + max-width: 46rem; + + .login-card { + width: 100%; + padding: 0; + border-radius: 1.5rem; + background: rgba(255, 255, 255, 0.98); + border: 1px solid rgba(255, 255, 255, 0.6); + backdrop-filter: blur(10px) saturate(120%); + box-shadow: 0 30px 80px rgba(24, 14, 84, 0.45); + overflow: hidden; + + &.split { + display: grid; + grid-template-columns: 1fr 1fr; + min-height: 22rem; + } + + .login-hero { + position: relative; + padding: 2.5rem; + background: linear-gradient(135deg, rgba(244, 245, 255, 0.95), rgba(233, 236, 255, 0.95)); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; + + .hero-particles { + position: absolute; + inset: 0; + pointer-events: none; + + span { + position: absolute; + width: 10px; + height: 10px; + border-radius: 50%; + background: rgba(109, 93, 246, 0.38); + box-shadow: 0 0 18px rgba(109, 93, 246, 0.4); + animation: floaty 7s ease-in-out infinite; + } + + span:nth-child(1) { + top: 18%; + left: 18%; + width: 6px; + height: 6px; + animation-delay: 0s; + } + + span:nth-child(2) { + top: 26%; + right: 22%; + width: 12px; + height: 12px; + animation-delay: 1s; + } + + span:nth-child(3) { + bottom: 30%; + left: 28%; + width: 7px; + height: 7px; + animation-delay: 2s; + } + + span:nth-child(4) { + bottom: 20%; + right: 30%; + width: 9px; + height: 9px; + animation-delay: 1.5s; + } + + span:nth-child(5) { + top: 55%; + left: 12%; + width: 5px; + height: 5px; + animation-delay: 2.8s; + } + + span:nth-child(6) { + top: 60%; + right: 15%; + width: 6px; + height: 6px; + animation-delay: 3.2s; + } + + span:nth-child(7) { + top: 10%; + right: 45%; + width: 4px; + height: 4px; + animation-delay: 1.8s; + } + + span:nth-child(8) { + bottom: 12%; + left: 45%; + width: 8px; + height: 8px; + animation-delay: 2.4s; + } + + span:nth-child(9) { + top: 42%; + right: 8%; + width: 5px; + height: 5px; + animation-delay: 3.6s; + } + + span:nth-child(10) { + top: 70%; + left: 6%; + width: 7px; + height: 7px; + animation-delay: 4.2s; + } + + span:nth-child(11) { + top: 34%; + left: 60%; + width: 6px; + height: 6px; + animation-delay: 1.2s; + } + + span:nth-child(12) { + bottom: 40%; + right: 40%; + width: 10px; + height: 10px; + animation-delay: 2.9s; + } + } + + .hero-icon { + width: 9.5rem; + height: 9.5rem; + border-radius: 50%; + background: rgba(109, 93, 246, 0.08); + border: 1px solid rgba(109, 93, 246, 0.25); + box-shadow: 0 0 28px rgba(109, 93, 246, 0.25), inset 0 0 18px rgba(109, 93, 246, 0.12); + display: flex; + align-items: center; + justify-content: center; + + img { + width: 6.3rem; + height: 6.3rem; + } + } + + .hero-text { + text-align: center; + } + + .hero-brand { + max-width: 12rem; + height: auto; + display: inline-block; + } + } + + .login-panel { + padding: 2.5rem 2.5rem 2.25rem; + display: flex; + flex-direction: column; + justify-content: center; + } + } + + .brand-text-only { + text-align: center; + font-size: 1.5rem; + font-weight: 800; + font-family: "Google Sans"; + color: #1f2a44; + letter-spacing: 0.02rem; + margin: 0 auto 0.3rem; + } + + .login-subtitle { + text-align: center; + color: #7b849c; + font-size: 0.9rem; + font-weight: 500; + margin-bottom: 1.5rem; + } + + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; + white-space: nowrap; + } + + // Form container + .form-login { + width: 100%; + padding: 0; + box-sizing: border-box; + + .errorbox { + margin-bottom: 1.25rem; + padding: 0.75rem 1rem; + border-radius: 0.9rem; + text-align: center; + color: #b42318; + background: rgba(255, 87, 87, 0.12); + border: 1px solid rgba(255, 87, 87, 0.3); + } + + // Input group styling + .input-group { + position: relative; + margin-bottom: 1.1rem; + display: flex; + align-items: center; + + // Icon styling + &::before { + .icon-base(); + position: absolute; + top: 50%; + left: 1.1rem; + z-index: 100; + font-size: 1.25rem; + color: #6b7280; + transform: translateY(-50%); + } + + // Input field + input { + display: block; + width: 100%; + padding: 0.9rem 1rem 0.9rem 3.2rem; + font-size: 0.95rem; + line-height: 1.5em; + color: #1f2937; + background-color: #f6f7fb; + background-clip: padding-box; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 999px; + outline: none; + box-sizing: border-box; + box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.06), 0 8px 18px rgba(15, 23, 42, 0.08); + transition: all 0.25s ease; + + &:focus { + border-color: rgba(109, 93, 246, 0.6); + background-color: #ffffff; + box-shadow: 0 0 0 4px rgba(109, 93, 246, 0.15); + } + + &::placeholder { + color: #8b93a7; + letter-spacing: 0.01rem; + } + } + + // Icon content + &.user-icon::before { + content: "\e971"; + } + + &.pass-icon::before { + content: "\e910"; + } + } + } + + // Submit button + .cbi-button-apply { + width: 100% !important; + min-height: 3rem; + margin: 1.2rem 0 0.2rem; + padding: 0.85rem 0; + font-size: 0.95rem; + font-weight: 600; + color: var(--white); + text-align: center; + letter-spacing: 0.08rem; + background: linear-gradient(135deg, #6d5df6, #4d8bff) !important; + border: none; + border-radius: 0.9rem; + outline: none; + cursor: pointer; + box-shadow: 0 16px 30px rgba(84, 88, 245, 0.35); + transition: all 0.3s ease !important; + + &:hover, + &:focus { + transform: translateY(-1px); + box-shadow: 0 22px 40px rgba(84, 88, 245, 0.45); + } + } + } + + // Footer + footer { + position: absolute; + bottom: 0; + z-index: 10; + display: flex; + justify-content: space-evenly; + width: 100%; + margin-top: auto; + padding: 0 0 30px; + color: var(--gray); + text-align: center; + line-height: 1.6rem; + font-size: 0.75rem; + + .ftc { + position: absolute; + bottom: 30px; + width: 100%; + } + + .luci-link { + display: block; + } + } + } +} + +@keyframes floaty { + 0% { + transform: translateY(0) scale(1); + opacity: 0.45; + } + + 50% { + transform: translateY(-16px) scale(1.2); + opacity: 1; + } + + 100% { + transform: translateY(0) scale(1); + opacity: 0.45; + } +} diff --git a/luci-theme-noobwrt/package-lock.json b/luci-theme-noobwrt/package-lock.json new file mode 100644 index 000000000..738e2114f --- /dev/null +++ b/luci-theme-noobwrt/package-lock.json @@ -0,0 +1,1534 @@ +{ + "name": "luci-theme-noobwrt", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "less": "^4.6.4", + "ttf2woff2": "^8.0.1" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/fs/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bufferstreams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bufferstreams/-/bufferstreams-4.0.0.tgz", + "integrity": "sha512-azX778/2VQ9K2uiYprSUKLgK2K6lR1KtJycJDsMg7u0+Cc994A9HyGaUKb01e/T+M8jse057429iKXurCaT35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^3.4.0", + "yerror": "^8.0.0" + }, + "engines": { + "node": ">=20.11.1" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/isexe": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/less": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/less/-/less-4.6.4.tgz", + "integrity": "sha512-OJmO5+HxZLLw0RLzkqaNHzcgEAQG7C0y3aMbwtCzIUFZsLMNNq/1IdAdHEycQ58CwUO3jPTHmoN+tE5I7FQxNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "copy-anything": "^3.0.5", + "parse-node-version": "^1.0.1" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/needle": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.5.0.tgz", + "integrity": "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "optional": true, + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.13.tgz", + "integrity": "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/ttf2woff2": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ttf2woff2/-/ttf2woff2-8.0.1.tgz", + "integrity": "sha512-nWSZLaXOgYtvgY6G0SFI8dVHsGWIchlnNMNRglT3Amp2WGy0GSPd9kLAkFd+HvEOzZ/aY6EUrpOF66QaPbipgg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "bufferstreams": "^4.0.0", + "debug": "^4.4.1", + "nan": "^2.22.2", + "node-gyp": "^11.2.0", + "yerror": "^8.0.0" + }, + "bin": { + "ttf2woff2": "bin/ttf2woff2.js" + }, + "engines": { + "node": ">=20.11.1" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yerror": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/yerror/-/yerror-8.0.0.tgz", + "integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.16.0" + } + } + } +} diff --git a/luci-theme-noobwrt/package.json b/luci-theme-noobwrt/package.json new file mode 100644 index 000000000..292725689 --- /dev/null +++ b/luci-theme-noobwrt/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "less": "^4.6.4", + "ttf2woff2": "^8.0.1" + } +} diff --git a/luci-theme-noobwrt/po/zh-cn b/luci-theme-noobwrt/po/zh-cn new file mode 120000 index 000000000..8d69574dd --- /dev/null +++ b/luci-theme-noobwrt/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-theme-noobwrt/po/zh_Hans/luci-theme-noobwrt.po b/luci-theme-noobwrt/po/zh_Hans/luci-theme-noobwrt.po new file mode 100644 index 000000000..34343ef84 --- /dev/null +++ b/luci-theme-noobwrt/po/zh_Hans/luci-theme-noobwrt.po @@ -0,0 +1,221 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8\n" + +msgid "NoobWrt Theme" +msgstr "NoobWrt 主题设置" + +msgid "NoobWrt Theme Settings" +msgstr "NoobWrt 主题设置" + +msgid "Customize the appearance, wallpaper, background effects, and toolbar panel of the NoobWrt LuCI theme." +msgstr "自定义 NoobWrt LuCI 主题的外观、壁纸、背景效果和工具栏面板。" + +msgid "Appearance" +msgstr "外观" + +msgid "Default Theme Mode" +msgstr "默认主题模式" + +msgid "Theme applied on page load when Automatic mode is disabled. Users can also toggle it manually using the toolbar sun/moon button." +msgstr "禁用自动模式时页面加载应用的主题模式。用户也可通过工具栏日/月按钮手动切换。" + +msgid "Light" +msgstr "亮色" + +msgid "Dark" +msgstr "暗色" + +msgid "Automatic Light / Dark Mode" +msgstr "自动亮色/暗色模式" + +msgid "Automatically switch to Dark mode from 6 PM to 6 AM and Light mode from 6 AM to 6 PM. When enabled, the schedule overrides the manual toolbar toggle." +msgstr "自动在 18:00 至 06:00 切换为暗色模式,06:00 至 18:00 切换为亮色模式。启用后,定时计划将覆盖工具栏手动切换。" + +msgid "Accent Color — Light Mode" +msgstr "主题色 — 亮色模式" + +msgid "Primary accent color used throughout the light theme." +msgstr "整个亮色主题使用的主要强调色。" + +msgid "Accent Color — Dark Mode" +msgstr "主题色 — 暗色模式" + +msgid "Primary accent color used throughout the dark theme." +msgstr "整个暗色主题使用的主要强调色。" + +msgid "Wallpaper" +msgstr "壁纸" + +msgid "Online Wallpaper Source" +msgstr "在线壁纸来源" + +msgid "Automatically fetch a wallpaper from an online source for the login page background. Set to None to use a custom uploaded image or the built-in default." +msgstr "自动从在线来源获取登录页背景壁纸。设为"无"可使用自定义上传图片或内置默认壁纸。" + +msgid "None (use custom / default)" +msgstr "无(使用自定义/默认)" + +msgid "Bing Daily Photo" +msgstr "必应每日图片" + +msgid "Lorem Picsum (Random)" +msgstr "随机图片(Lorem Picsum)" + +msgid "Wikimedia Picture of the Day" +msgstr "维基媒体每日图片" + +msgid "Background Effects" +msgstr "背景效果" + +msgid "Blur Radius — Light Mode" +msgstr "模糊半径 — 亮色模式" + +msgid "Login page background blur radius in pixels for light mode. Default: 10" +msgstr "亮色模式下登录页背景模糊半径(像素)。默认:10" + +msgid "Blur Radius — Dark Mode" +msgstr "模糊半径 — 暗色模式" + +msgid "Login page background blur radius in pixels for dark mode. Default: 10" +msgstr "暗色模式下登录页背景模糊半径(像素)。默认:10" + +msgid "Background Opacity — Light Mode" +msgstr "背景透明度 — 亮色模式" + +msgid "Overlay opacity on the login page background for light mode. Range: 0.0 (transparent) to 1.0 (opaque). Default: 0.8" +msgstr "亮色模式下登录页背景遮罩透明度。范围:0.0(透明)至 1.0(不透明)。默认:0.8" + +msgid "Background Opacity — Dark Mode" +msgstr "背景透明度 — 暗色模式" + +msgid "Overlay opacity on the login page background for dark mode. Default: 0.8" +msgstr "暗色模式下登录页背景遮罩透明度。默认:0.8" + +msgid "Toolbar Panel" +msgstr "工具栏面板" + +msgid "Toolbar Panel Items" +msgstr "工具栏面板项目" + +msgid "Upload a custom image to use as the login page background. Supported formats: JPG, PNG, WebP (max 10 MB)." +msgstr "上传自定义图片作为登录页背景。支持格式:JPG、PNG、WebP(最大 10 MB)。" + +msgid "No custom wallpaper — using default" +msgstr "未设置自定义壁纸 — 使用默认" + +msgid "Upload Wallpaper..." +msgstr "上传壁纸..." + +msgid "Revert to Default" +msgstr "恢复默认" + +msgid "Quick-access links displayed in the right-side toolbar panel on every page. Items are rendered in ascending Order number. Use the Add button below to create new entries." +msgstr "显示在每个页面右侧工具栏面板中的快捷链接。按排序数字升序显示。点击下方添加按钮创建新条目。" + +msgid "Quick-access links displayed in the right-side toolbar panel on every page. " +msgstr "显示在每个页面右侧工具栏面板中的快捷链接。" + +msgid "Enable" +msgstr "启用" + +msgid "Label" +msgstr "标签" + +msgid "URL / Path" +msgstr "URL / 路径" + +msgid "Icon" +msgstr "图标" + +msgid "Home" +msgstr "主页" + +msgid "Signal / Status" +msgstr "信号 / 状态" + +msgid "Cell / Modem" +msgstr "蜂窝 / 调制解调器" + +msgid "SMS" +msgstr "短信" + +msgid "Network / Data Usage" +msgstr "网络 / 流量" + +msgid "NAS / Storage" +msgstr "NAS / 存储" + +msgid "WiFi / Wireless" +msgstr "WiFi / 无线" + +msgid "Firewall" +msgstr "防火墙" + +msgid "Settings / System" +msgstr "设置 / 系统" + +msgid "Terminal" +msgstr "终端" + +msgid "VPN" +msgstr "VPN" + +msgid "Files" +msgstr "文件" + +msgid "Info / Log" +msgstr "信息 / 日志" + +msgid "Order" +msgstr "排序" + +msgid "Lower numbers appear first in the toolbar." +msgstr "数字越小越靠前显示。" + +msgid "About" +msgstr "关于" + +msgid "A clean modern LuCI theme for OpenWrt routers." +msgstr "一款简洁现代的 OpenWrt LuCI 主题。" + +msgid "Developer" +msgstr "开发者" + +msgid "Repository" +msgstr "代码仓库" + +msgid "Contributors" +msgstr "贡献者" + +msgid "View on GitHub" +msgstr "在 GitHub 上查看" + +msgid "License" +msgstr "许可证" + +msgid "Issues / Support" +msgstr "问题 / 支持" + +msgid "Open an issue" +msgstr "提交 Issue" + +msgid "Made with \u2665 for the OpenWrt community" +msgstr "用 \u2665 为 OpenWrt 社区制作" + +msgid "Wallpaper uploaded! Refresh the login page to see it." +msgstr "壁纸已上传!刷新登录页面即可查看。" + +msgid "Failed to save wallpaper." +msgstr "壁纸保存失败。" + +msgid "Upload error: " +msgstr "上传错误:" + +msgid "Remove custom wallpaper and revert to default?" +msgstr "删除自定义壁纸并恢复默认?" + +msgid "Reverted to default wallpaper." +msgstr "已恢复默认壁纸。" + +msgid "Could not remove: " +msgstr "无法删除:" diff --git a/luci-theme-noobwrt/root/etc/uci-defaults/30_luci-theme-noobwrt b/luci-theme-noobwrt/root/etc/uci-defaults/30_luci-theme-noobwrt new file mode 100644 index 000000000..6a86420f8 --- /dev/null +++ b/luci-theme-noobwrt/root/etc/uci-defaults/30_luci-theme-noobwrt @@ -0,0 +1,67 @@ +#!/bin/sh + +# Register NoobWrt as a LuCI theme (skip on upgrade) +if [ "$PKG_UPGRADE" != 1 ]; then + uci get luci.themes.NoobWrt >/dev/null 2>&1 || \ + uci batch <<-EOF + set luci.themes.NoobWrt=/luci-static/noobwrt + set luci.main.mediaurlbase=/luci-static/noobwrt + commit luci + EOF +fi + +# Ensure the config file exists +touch /etc/config/noobwrt + +# Initialise global section type (only if section absent) +uci get noobwrt.global >/dev/null 2>&1 || uci set noobwrt.global=global + +# Set each global option only if not already present +uci get noobwrt.global.mode >/dev/null 2>&1 || uci set noobwrt.global.mode=normal +uci get noobwrt.global.primary >/dev/null 2>&1 || uci set noobwrt.global.primary='#7689f4' +uci get noobwrt.global.dark_primary >/dev/null 2>&1 || uci set noobwrt.global.dark_primary='#7c8ff5' +uci get noobwrt.global.blur >/dev/null 2>&1 || uci set noobwrt.global.blur=10 +uci get noobwrt.global.blur_dark >/dev/null 2>&1 || uci set noobwrt.global.blur_dark=10 +uci get noobwrt.global.transparency >/dev/null 2>&1 || uci set noobwrt.global.transparency=0.8 +uci get noobwrt.global.transparency_dark >/dev/null 2>&1 || uci set noobwrt.global.transparency_dark=0.8 +uci get noobwrt.global.online_wallpaper >/dev/null 2>&1 || uci set noobwrt.global.online_wallpaper=none +uci get noobwrt.global.use_exact_resolution >/dev/null 2>&1 || uci set noobwrt.global.use_exact_resolution=1 +uci get noobwrt.global.auto_dark_mode >/dev/null 2>&1 || uci set noobwrt.global.auto_dark_mode=0 + +uci commit noobwrt + +# Helper: seed a toolbar_item section and its fields if missing +seed_toolbar() { + local name="$1" title="$2" url="$3" icon="$4" enabled="$5" order="$6" + uci get noobwrt."$name" >/dev/null 2>&1 || uci set noobwrt."$name"=toolbar_item + uci get noobwrt."$name".title >/dev/null 2>&1 || uci set noobwrt."$name".title="$title" + uci get noobwrt."$name".url >/dev/null 2>&1 || uci set noobwrt."$name".url="$url" + uci get noobwrt."$name".icon >/dev/null 2>&1 || uci set noobwrt."$name".icon="$icon" + uci get noobwrt."$name".enabled >/dev/null 2>&1 || uci set noobwrt."$name".enabled="$enabled" + uci get noobwrt."$name".order >/dev/null 2>&1 || uci set noobwrt."$name".order="$order" +} + +seed_toolbar toolbar_home 'Home' '/cgi-bin/luci/admin/status/overview' home.png 1 1 +seed_toolbar toolbar_status 'Status' '/cgi-bin/luci/admin/modem/modemdata' signal.png 1 2 +seed_toolbar toolbar_modem 'Modem' '/cgi-bin/luci/admin/modem/qmodem' cell.png 0 3 +seed_toolbar toolbar_sms 'SMS' '/cgi-bin/luci/admin/modem/luci-app-sms-tool-js' sms.png 1 4 +seed_toolbar toolbar_data 'Data Usage' '/cgi-bin/luci/admin/tools/netstat' network.png 1 5 +seed_toolbar toolbar_nas 'NAS' '/cgi-bin/luci/admin/services/samba4' nas.png 0 6 +seed_toolbar toolbar_wireless 'Wireless' '/cgi-bin/luci/admin/network/wireless' wifi.png 1 7 +seed_toolbar toolbar_firewall 'Firewall' '/cgi-bin/luci/admin/network/firewall' firewall.png 0 8 +seed_toolbar toolbar_log 'System Log' '/cgi-bin/luci/admin/status/logs' info.png 0 9 +seed_toolbar toolbar_terminal 'Terminal' '/cgi-bin/luci/admin/system/ttyd' terminal.png 1 10 +seed_toolbar toolbar_vpn 'VPN' '/cgi-bin/luci/admin/services/passwall' vpn.png 0 11 +seed_toolbar toolbar_files 'Files' '/cgi-bin/luci/admin/nas/tinyfilemanager' files.png 1 12 +seed_toolbar toolbar_system 'Settings' '/cgi-bin/luci/admin/system/noobwrt' settings.png 1 13 + +uci commit noobwrt + +# Ensure the rpcd wallpaper script is executable so ubus registers it +chmod +x /usr/libexec/rpcd/luci.noobwrt_wallpaper 2>/dev/null + +# Reload rpcd so luci.noobwrt_wallpaper is registered on first install +# (only needed when rpcd is already running; harmless if it isn't yet) +/etc/init.d/rpcd reload 2>/dev/null + +exit 0 diff --git a/luci-theme-noobwrt/root/usr/libexec/rpcd/luci.noobwrt_wallpaper b/luci-theme-noobwrt/root/usr/libexec/rpcd/luci.noobwrt_wallpaper new file mode 100755 index 000000000..f15c4d87f --- /dev/null +++ b/luci-theme-noobwrt/root/usr/libexec/rpcd/luci.noobwrt_wallpaper @@ -0,0 +1,136 @@ +#!/bin/sh + +. /lib/functions.sh +. /usr/share/libubox/jshn.sh + +CUSTOM_BG="/www/luci-static/noobwrt/background/custom.jpg" +UPLOAD_TMP="/tmp/noobwrt_wallpaper.tmp" + +# Online wallpaper source (bing/picsum/wikimedia or none) — no API key required +WEB_PIC_SRC="$(uci -q get noobwrt.@global[0].online_wallpaper || echo 'bing')" + +# Normalise legacy / unknown values to 'bing' so the case always matches +case "$WEB_PIC_SRC" in +bing|picsum|wikimedia|none) ;; +*) WEB_PIC_SRC="bing" ;; +esac + +CACHE="/var/run/noobwrt_${WEB_PIC_SRC}.url" +WRLOCK="/var/lock/noobwrt_${WEB_PIC_SRC}.lock" + +fetch_pic_url() { + case "$WEB_PIC_SRC" in + bing) + local picpath="$(wget -T5 -qO- \ + "https://www.bing.com/HPImageArchive.aspx?format=js&n=1" | + jsonfilter -qe '@.images[0].url' | + sed 's/1920x1080/UHD/g')" + [ -n "${picpath}" ] && echo "https://www.bing.com${picpath}" + ;; + picsum) + # Lorem Picsum — free random photo, no API key needed + # Follow the 302 redirect and cache the direct CDN image URL + local seed="$(($(date '+%s') % 1000))" + local cdnurl="$(wget -T5 -S -O /dev/null \ + "https://picsum.photos/seed/${seed}/1920/1080" 2>&1 | + grep -i 'location:' | head -n1 | + sed 's/.*[Ll]ocation: *//' | tr -d '\r\n')" + if [ -n "$cdnurl" ]; then + echo "$cdnurl" + else + echo "https://picsum.photos/seed/${seed}/1920/1080" + fi + ;; + wikimedia) + # Wikimedia Commons Picture of the Day — free, no API key needed + # Fetch the featured image URL from the Wikimedia REST API + local today="$(date '+%Y/%m/%d')" + wget -T5 -qO- \ + "https://api.wikimedia.org/feed/v1/wikipedia/en/featured/${today}" | + jsonfilter -qe '@.image.image.source' 2>/dev/null + ;; + esac +} + +try_update() { + local lock="$WRLOCK" + exec 200>"$lock" + + if flock -n 200 >/dev/null 2>&1; then + local picurl="$(fetch_pic_url)" + if [ -n "$picurl" ]; then + echo "$picurl" | tee "$CACHE" + else + if [ -s "$CACHE" ]; then + cat "$CACHE" + else + touch "$CACHE" + fi + fi + flock -u 200 >/dev/null 2>&1 + elif [ -s "$CACHE" ]; then + cat "$CACHE" + fi +} + +case "$1" in +"list") + json_init + json_add_object "get_url" + json_close_object + json_add_object "set_custom" + json_close_object + json_add_object "remove_custom" + json_close_object + json_dump + json_cleanup + ;; +"call") + case "$2" in + "set_custom") + if [ -f "$UPLOAD_TMP" ]; then + mv "$UPLOAD_TMP" "$CUSTOM_BG" && chmod 0644 "$CUSTOM_BG" + echo '{ "result": 0 }' + else + echo '{ "result": 1, "error": "tmp file not found" }' + fi + ;; + "remove_custom") + rm -f "$CUSTOM_BG" + echo '{ "result": 0 }' + ;; + "get_url") + read -r input + if [ "$WEB_PIC_SRC" = "none" ]; then + echo '{ "url": "" }' + exit 0 + fi + if [ -f "$CACHE" ]; then + idle_t="$(($(date '+%s') - $(date -r "$CACHE" '+%s' 2>/dev/null || echo '0')))" + if [ -s "$CACHE" ]; then + if [ "$idle_t" -le 43200 ]; then + json_init + json_add_string "url" "$(cat "$CACHE")" + json_dump + json_cleanup + exit 0 + fi + else + # Empty cache file means a fetch is in progress or recently + # failed — suppress retry for 120 s to avoid hammering the API + if [ "$idle_t" -le 120 ]; then + echo '{ "url": "" }' + exit 0 + fi + fi + fi + + json_init + json_add_string "url" "$(try_update)" + json_dump + json_cleanup + exit 0 + ;; + esac + ;; +esac diff --git a/luci-theme-noobwrt/root/usr/share/luci/menu.d/luci-theme-noobwrt.json b/luci-theme-noobwrt/root/usr/share/luci/menu.d/luci-theme-noobwrt.json new file mode 100644 index 000000000..905b5c0eb --- /dev/null +++ b/luci-theme-noobwrt/root/usr/share/luci/menu.d/luci-theme-noobwrt.json @@ -0,0 +1,10 @@ +{ + "admin/system/noobwrt": { + "title": "NoobWrt Theme", + "order": 80, + "action": { + "type": "view", + "path": "noobwrt/settings" + } + } +} diff --git a/luci-theme-noobwrt/root/usr/share/rpcd/acl.d/luci-theme-noobwrt.json b/luci-theme-noobwrt/root/usr/share/rpcd/acl.d/luci-theme-noobwrt.json new file mode 100644 index 000000000..b1d677c7d --- /dev/null +++ b/luci-theme-noobwrt/root/usr/share/rpcd/acl.d/luci-theme-noobwrt.json @@ -0,0 +1,21 @@ +{ + "luci-theme-noobwrt": { + "description": "Grant UCI read/write access and wallpaper upload for luci-theme-noobwrt", + "read": { + "uci": [ "noobwrt" ], + "file": { + "/www/luci-static/noobwrt/background/*": [ "read" ] + } + }, + "write": { + "uci": [ "noobwrt" ], + "cgi-io": [ "upload" ], + "file": { + "/tmp/noobwrt_wallpaper.tmp": [ "write" ] + }, + "ubus": { + "luci.noobwrt_wallpaper": [ "set_custom", "remove_custom" ] + } + } + } +} diff --git a/luci-theme-noobwrt/screenshots/banner.png b/luci-theme-noobwrt/screenshots/banner.png new file mode 100644 index 000000000..6a7108396 Binary files /dev/null and b/luci-theme-noobwrt/screenshots/banner.png differ diff --git a/luci-theme-noobwrt/screenshots/bg.png b/luci-theme-noobwrt/screenshots/bg.png new file mode 100644 index 000000000..2e3030aed Binary files /dev/null and b/luci-theme-noobwrt/screenshots/bg.png differ diff --git a/luci-theme-noobwrt/screenshots/pc_1.png b/luci-theme-noobwrt/screenshots/pc_1.png new file mode 100644 index 000000000..94407525e Binary files /dev/null and b/luci-theme-noobwrt/screenshots/pc_1.png differ diff --git a/luci-theme-noobwrt/screenshots/sc_desktop_dark.png b/luci-theme-noobwrt/screenshots/sc_desktop_dark.png new file mode 100644 index 000000000..0946aadd3 Binary files /dev/null and b/luci-theme-noobwrt/screenshots/sc_desktop_dark.png differ diff --git a/luci-theme-noobwrt/screenshots/sc_desktop_light.png b/luci-theme-noobwrt/screenshots/sc_desktop_light.png new file mode 100644 index 000000000..c9394856b Binary files /dev/null and b/luci-theme-noobwrt/screenshots/sc_desktop_light.png differ diff --git a/luci-theme-noobwrt/screenshots/sc_mobile.png b/luci-theme-noobwrt/screenshots/sc_mobile.png new file mode 100644 index 000000000..a09848040 Binary files /dev/null and b/luci-theme-noobwrt/screenshots/sc_mobile.png differ diff --git a/luci-theme-noobwrt/screenshots/sc_mobile_dark.png b/luci-theme-noobwrt/screenshots/sc_mobile_dark.png new file mode 100644 index 000000000..c82fb37ce Binary files /dev/null and b/luci-theme-noobwrt/screenshots/sc_mobile_dark.png differ diff --git a/luci-theme-noobwrt/screenshots/sc_mobile_login.png b/luci-theme-noobwrt/screenshots/sc_mobile_login.png new file mode 100644 index 000000000..b92bb8c33 Binary files /dev/null and b/luci-theme-noobwrt/screenshots/sc_mobile_login.png differ diff --git a/luci-theme-noobwrt/screenshots/sc_settings.png b/luci-theme-noobwrt/screenshots/sc_settings.png new file mode 100644 index 000000000..02e7e12a6 Binary files /dev/null and b/luci-theme-noobwrt/screenshots/sc_settings.png differ diff --git a/luci-theme-noobwrt/ucode/template/themes/noobwrt/footer.ut b/luci-theme-noobwrt/ucode/template/themes/noobwrt/footer.ut new file mode 100644 index 000000000..bf791bce2 --- /dev/null +++ b/luci-theme-noobwrt/ucode/template/themes/noobwrt/footer.ut @@ -0,0 +1,45 @@ +{# + NoobWrt - A clean modern LuCI theme for OpenWrt routers. + https://github.com/nooblk-98/luci-theme-noobwrt + Licensed to the public under the Apache License 2.0 +-#} + +
+ +
+
+{% if (ctx.authsession): %} +{% include("themes/" + theme + "/toolbar") %} +{% endif %} + + + + diff --git a/luci-theme-noobwrt/ucode/template/themes/noobwrt/footer_login.ut b/luci-theme-noobwrt/ucode/template/themes/noobwrt/footer_login.ut new file mode 100644 index 000000000..5681fbf1b --- /dev/null +++ b/luci-theme-noobwrt/ucode/template/themes/noobwrt/footer_login.ut @@ -0,0 +1,34 @@ +{# + NoobWrt - A clean modern LuCI theme for OpenWrt routers. + https://github.com/nooblk-98/luci-theme-noobwrt + Licensed to the public under the Apache License 2.0 +-#} + +
+ +
+
+ + + + + diff --git a/luci-theme-noobwrt/ucode/template/themes/noobwrt/header.ut b/luci-theme-noobwrt/ucode/template/themes/noobwrt/header.ut new file mode 100644 index 000000000..9a90c41af --- /dev/null +++ b/luci-theme-noobwrt/ucode/template/themes/noobwrt/header.ut @@ -0,0 +1,169 @@ +{# + NoobWrt - A clean modern LuCI theme for OpenWrt routers. + https://github.com/nooblk-98/luci-theme-noobwrt + Licensed to the public under the Apache License 2.0 +-#} + +{% + import { readfile, access } from 'fs'; + import { cursor } from 'uci'; + import { srand } from 'math'; + import { getuid, getspnam } from 'luci.core'; + + const boardinfo = ubus.call("system", "board"); + const hostname = striptags(boardinfo?.hostname ?? '?'); + + let cfg = cursor(); + + //send as HTML5 + http.prepare_content('text/html; charset=UTF-8'); + + srand(+substr(reverse(time() + ""), 0, 8)); + + //Custom settings — defaults match uci-defaults seed values + let primary = '#5e72e4'; + let dark_primary = '#7c8ff5'; + let blur_radius = '10'; + let blur_radius_dark = '10'; + let blur_opacity = '0.8'; + let blur_opacity_dark = '0.8'; + let mode = 'normal'; + + const dark_css = readfile('/www/luci-static/noobwrt/css/dark.css'); + if (access('/etc/config/noobwrt')) { + primary = cfg.get_first('noobwrt', 'global', 'primary') ?? primary; + dark_primary = cfg.get_first('noobwrt', 'global', 'dark_primary') ?? dark_primary; + blur_radius = cfg.get_first('noobwrt', 'global', 'blur') ?? blur_radius; + blur_radius_dark = cfg.get_first('noobwrt', 'global', 'blur_dark') ?? blur_radius_dark; + blur_opacity = cfg.get_first('noobwrt', 'global', 'transparency') ?? blur_opacity; + blur_opacity_dark = cfg.get_first('noobwrt', 'global', 'transparency_dark') ?? blur_opacity_dark; + mode = cfg.get_first('noobwrt', 'global', 'mode') ?? mode; + } + const bar_color = (mode == 'dark') ? dark_primary : primary; + +-%} + + + + + + + {{ hostname }}{{ node?.title ? ` - ${striptags(node.title)}` : '' }} - LuCI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if (mode == 'dark'): %} + + {% endif -%} + + + {% if (node?.css): %} + + {% endif -%} + {% if (css): %} + + {% endif -%} + + + + + + + +
+ +
+
+
+
+ +
+
+
+
+
+
+
+ {% if (getuid() == 0 && getspnam('root')?.pwdp === ''): %} +
+

{{ _('No password set!') }}

+

{{ _('There is no password set on this router. Please configure a root password to protect the web interface.') }}

+ {% if (dispatcher.lookup("admin/system/admin")): %} + + {% endif %} +
+ {% endif %} + + + + + + diff --git a/luci-theme-noobwrt/ucode/template/themes/noobwrt/header_login.ut b/luci-theme-noobwrt/ucode/template/themes/noobwrt/header_login.ut new file mode 100644 index 000000000..d7b9bd6cd --- /dev/null +++ b/luci-theme-noobwrt/ucode/template/themes/noobwrt/header_login.ut @@ -0,0 +1,107 @@ +{# + NoobWrt - A clean modern LuCI theme for OpenWrt routers. + https://github.com/nooblk-98/luci-theme-noobwrt + Licensed to the public under the Apache License 2.0 +-#} + +{% + import { readfile, access } from 'fs'; + import { cursor } from 'uci'; + import { srand } from 'math'; + + const boardinfo = ubus.call("system", "board"); + const hostname = striptags(boardinfo?.hostname ?? '?'); + + let cfg = cursor(); + + //send as HTML5 + http.prepare_content('text/html; charset=UTF-8'); + + srand(+substr(reverse(time() + ""), 0, 8)); + + //Custom settings + let mode = 'normal'; + let bar_color = '#5e72e4'; + let primary, dark_primary, blur_radius, blur_radius_dark, blur_opacity, blur_opacity_dark; + const dark_css = readfile('/www/luci-static/noobwrt/css/dark.css'); + if (access('/etc/config/noobwrt')) { + primary = cfg.get_first('noobwrt', 'global', 'primary'); + dark_primary = cfg.get_first('noobwrt', 'global', 'dark_primary'); + blur_radius = cfg.get_first('noobwrt', 'global', 'blur'); + blur_radius_dark = cfg.get_first('noobwrt', 'global', 'blur_dark'); + blur_opacity = cfg.get_first('noobwrt', 'global', 'transparency'); + blur_opacity_dark = cfg.get_first('noobwrt', 'global', 'transparency_dark'); + mode = cfg.get_first('noobwrt', 'global', 'mode'); + bar_color = (mode == 'dark') ? dark_primary : primary; + } + +-%} + + + + + + + {{ hostname }}{{ node?.title ? ` - ${striptags(node.title)}` : '' }} - LuCI + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% if (node?.css): %} + + {% endif -%} + {% if (css): %} + + {% endif -%} + + + + diff --git a/luci-theme-noobwrt/ucode/template/themes/noobwrt/out_header_login.ut b/luci-theme-noobwrt/ucode/template/themes/noobwrt/out_header_login.ut new file mode 100644 index 000000000..54b599faf --- /dev/null +++ b/luci-theme-noobwrt/ucode/template/themes/noobwrt/out_header_login.ut @@ -0,0 +1,12 @@ +{# + Copyright 2008 Steven Barth + Copyright 2008-2019 Jo-Philipp Wich + Licensed to the public under the Apache License 2.0. +-#} + +{% + if (!ctx.template_header_sent) { + include("themes/" + theme + "/header_login"); + ctx.template_header_sent = true; + } +%} diff --git a/luci-theme-noobwrt/ucode/template/themes/noobwrt/sysauth.ut b/luci-theme-noobwrt/ucode/template/themes/noobwrt/sysauth.ut new file mode 100644 index 000000000..e8b9cbbd6 --- /dev/null +++ b/luci-theme-noobwrt/ucode/template/themes/noobwrt/sysauth.ut @@ -0,0 +1,158 @@ +{# + NoobWrt - A clean modern LuCI theme for OpenWrt routers. + https://github.com/nooblk-98/luci-theme-noobwrt + Licensed to the public under the Apache License 2.0 +-#} + +{% include("themes/" + theme + "/out_header_login") %} +{% + import * as fs from 'fs'; + import { cursor } from 'uci'; + import { rand } from 'math'; + + let cfg = cursor(); + + //Fetch Local Background Media + + const imageTypes = " jpg jpeg png gif webp "; + const videoTypes = " mp4 webm "; + const allTypes = imageTypes + videoTypes; + + function fetchMedia(path, themeDir) { + let backgroundTable = []; + for (f in (fs.lsdir(path))) { + let parts = split(f, '.'); + let ext = lc(parts?.[length(parts) - 1]); + if (ext && index(allTypes, " " + ext + " ") != -1) { + let bg = {}; + bg.type = ext; + bg.url = themeDir + f; + push(backgroundTable, bg); + } + } + return backgroundTable; + } + + function selectBackground(themeDir) { + let bgUrl = media + "/img/bg1.jpg"; + let backgroundType = "Image"; + let mimeType = ""; + + if (fs.access("/etc/config/noobwrt")) { + let online_wallpaper = cfg.get_first('noobwrt', 'global', 'online_wallpaper') ?? (cfg.get_first('noobwrt', 'global', 'bing_background') == '1' ? 'bing' : null); + if (online_wallpaper && online_wallpaper != "none") { + const picurl = ubus.call("luci.noobwrt_wallpaper", "get_url") ?? {}; + if (picurl?.url) { + bgUrl = picurl.url; + return {bgUrl, backgroundType, mimeType}; + } + } + } + + let background = fetchMedia("/www" + themeDir, themeDir); + if ( length(background) > 0 ) { + let currentBg = background[(rand() % length(background))]; + bgUrl = currentBg.url; + if (index(videoTypes, " " + currentBg.type + " ") != -1) { + backgroundType = "Video"; + mimeType = "video/" + currentBg.type; + } + } + + return {bgUrl, backgroundType, mimeType}; + } + + const boardinfo = ubus.call("system", "board"); + const hostname = striptags(boardinfo?.hostname ?? '?'); + const themeDir = media + "/background/"; + const bgArry = selectBackground(themeDir); + +%} + +